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
|
path = app/src/stable/assets/sources/dailymotion
|
||||||
url = ../plugins/dailymotion.git
|
url = ../plugins/dailymotion.git
|
||||||
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||||
path = app/src/stable/assets/sources/apple-podcast
|
path = app/src/stable/assets/sources/apple-podcasts
|
||||||
url = ../plugins/apple-podcasts.git
|
url = ../plugins/apple-podcasts.git
|
||||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||||
path = app/src/unstable/assets/sources/apple-podcasts
|
path = app/src/unstable/assets/sources/apple-podcasts
|
||||||
|
|
|
@ -197,7 +197,7 @@ dependencies {
|
||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
|
|
|
@ -156,7 +156,6 @@
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
|
|
|
@ -263,6 +263,10 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||||
this.rating = obj.rating ?? null; //IRating
|
this.rating = obj.rating ?? null; //IRating
|
||||||
this.subtitles = obj.subtitles ?? [];
|
this.subtitles = obj.subtitles ?? [];
|
||||||
this.isShort = !!obj.isShort ?? false;
|
this.isShort = !!obj.isShort ?? false;
|
||||||
|
|
||||||
|
if (obj.getContentRecommendations) {
|
||||||
|
this.getContentRecommendations = obj.getContentRecommendations
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
||||||
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
|
||||||
|
val urlData = if (this.startsWith("polycentric://")) {
|
||||||
|
this.substring("polycentric://".length)
|
||||||
|
} else this;
|
||||||
|
|
||||||
|
val urlBytes = urlData.base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||||
|
if (urlInfo.urlType != 4L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||||
|
return dataLink
|
||||||
|
}
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
|
||||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
|
||||||
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
|
||||||
addServer(PolycentricCache.SERVER)
|
|
||||||
}
|
|
||||||
|
|
||||||
val exceptions = fullyBackfillServers()
|
|
||||||
for (pair in exceptions) {
|
|
||||||
val server = pair.key
|
|
||||||
val exception = pair.value
|
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement(
|
|
||||||
"backfill-failed",
|
|
||||||
"Backfill failed",
|
|
||||||
"Failed to backfill server $server. $exception",
|
|
||||||
AnnouncementType.SESSION_RECURRING
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -356,7 +356,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
|
@ -380,6 +380,8 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||||
|
var preferOriginalAudio: Boolean = true;
|
||||||
|
|
||||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
|
@ -644,6 +646,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
|
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||||
|
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||||
var clearCookiesOnLogout: Boolean = true;
|
var clearCookiesOnLogout: Boolean = true;
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,36 @@ class UISlideOverlays {
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
||||||
|
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
||||||
|
|
||||||
|
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addPlaylistOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text.trim()
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlaylistOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
StatePlayer.instance.saveQueueAsPlaylist(text);
|
||||||
|
UIDialogs.appToast("Playlist [${text}] created");
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
}, false));
|
||||||
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
|
@ -337,7 +367,9 @@ class UISlideOverlays {
|
||||||
call = {
|
call = {
|
||||||
selectedVideoVariant = it
|
selectedVideoVariant = it
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
if (audioButtons.isEmpty()){
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
))
|
))
|
||||||
|
@ -372,7 +404,7 @@ class UISlideOverlays {
|
||||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, resolvedPlaylistUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
|
||||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
|
@ -419,7 +451,7 @@ class UISlideOverlays {
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(
|
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_movie,
|
R.drawable.ic_movie,
|
||||||
container.context.getString(R.string.none),
|
container.context.getString(R.string.none),
|
||||||
|
@ -432,7 +464,7 @@ class UISlideOverlays {
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
)) +
|
)) else listOf()) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
|
@ -909,7 +941,7 @@ class UISlideOverlays {
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
if(!isLimited)
|
if(!isLimited && !video.isLive)
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_download,
|
R.drawable.ic_download,
|
||||||
|
@ -1045,8 +1077,9 @@ class UISlideOverlays {
|
||||||
StatePlayer.TYPE_WATCHLATER,
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
call = {
|
||||||
UIDialogs.appToast("Added to watch later", false);
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||||
|
UIDialogs.appToast("Added to watch later", false);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "LoginActivity";
|
private val TAG = "LoginActivity";
|
||||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
|
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
||||||
|
|
||||||
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||||
|
|
||||||
|
|
|
@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
processHandle.addServer(PolycentricCache.SERVER);
|
processHandle.addServer(ApiMethods.SERVER);
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|
|
@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.KeyPair
|
import com.futo.polycentric.core.KeyPair
|
||||||
import com.futo.polycentric.core.Process
|
import com.futo.polycentric.core.Process
|
||||||
import com.futo.polycentric.core.ProcessSecret
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
|
@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
finish();
|
finish();
|
||||||
|
|
|
@ -21,10 +21,8 @@ import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
@ -32,8 +30,10 @@ import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toBase64Url
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|
|
@ -101,7 +101,8 @@ class SyncHomeActivity : AppCompatActivity() {
|
||||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||||
val connected = session?.connected ?: false
|
val connected = session?.connected ?: false
|
||||||
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
||||||
.setName(publicKey)
|
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
||||||
|
//TODO: also display public key?
|
||||||
.setStatus(if (connected) "Connected" else "Disconnected")
|
.setStatus(if (connected) "Connected" else "Disconnected")
|
||||||
return syncDeviceView
|
return syncDeviceView
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
||||||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||||
|
|
||||||
current += bytesToSend.toLong()
|
current += bytesToSend.toLong()
|
||||||
if (current >= end) {
|
if (current > end) {
|
||||||
Logger.i(TAG, "Expected amount of bytes sent")
|
Logger.i(TAG, "Expected amount of bytes sent")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@ class AudioUrlSource(
|
||||||
override val codec: String = "",
|
override val codec: String = "",
|
||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false
|
override var priority: Boolean = false,
|
||||||
|
override var original: Boolean = false
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
@ -36,7 +37,9 @@ class AudioUrlSource(
|
||||||
source.container,
|
source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language,
|
source.language,
|
||||||
source.duration
|
source.duration,
|
||||||
|
source.priority,
|
||||||
|
source.original
|
||||||
);
|
);
|
||||||
ret.streamMetaData = streamData;
|
ret.streamMetaData = streamData;
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
|
||||||
override val language: String,
|
override val language: String,
|
||||||
override val duration: Long?,
|
override val duration: Long?,
|
||||||
override val priority: Boolean,
|
override val priority: Boolean,
|
||||||
|
override val original: Boolean,
|
||||||
val url: String
|
val url: String
|
||||||
) : IAudioUrlSource {
|
) : IAudioUrlSource {
|
||||||
override fun getAudioUrl(): String {
|
override fun getAudioUrl(): String {
|
||||||
|
|
|
@ -8,4 +8,5 @@ interface IAudioSource {
|
||||||
val language : String;
|
val language : String;
|
||||||
val duration : Long?;
|
val duration : Long?;
|
||||||
val priority: Boolean;
|
val priority: Boolean;
|
||||||
|
val original: Boolean;
|
||||||
}
|
}
|
|
@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||||
override val duration: Long? = null;
|
override val duration: Long? = null;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
val filePath : String;
|
val filePath : String;
|
||||||
val fileSize: Long;
|
val fileSize: Long;
|
||||||
|
@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||||
return LocalAudioSource(
|
return LocalAudioSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
fileSize,
|
fileSize,
|
||||||
source.bitrate,
|
source.bitrate,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language
|
source.language
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
|
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||||
return LocalVideoSource(
|
return LocalVideoSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
|
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||||
source.width,
|
source.width,
|
||||||
source.height,
|
source.height,
|
||||||
source.duration,
|
source.duration,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.bitrate?:0
|
source.bitrate?:0
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,8 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
||||||
val contextName = "AudioUrlSource";
|
val contextName = "AudioUrlSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
|
@ -35,6 +37,7 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
||||||
|
|
||||||
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioUrl() : String {
|
override fun getAudioUrl() : String {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
@ -14,13 +16,14 @@ import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val codec: String;
|
override val codec: String;
|
||||||
override val bitrate: Int;
|
override val bitrate: Int;
|
||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean;
|
override val priority: Boolean;
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
|
@ -29,17 +32,21 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||||
|
|
||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||||
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,15 +57,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
val plugin = _plugin.getUnderlyingPlugin();
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient)
|
if(_plugin is DevJSClient)
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
|
||||||
var manifest: String?;
|
var manifest: String?;
|
||||||
fun generate(): String?;
|
fun generate(): String?;
|
||||||
}
|
}
|
||||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val width: Int;
|
override val width: Int;
|
||||||
override val height: Int;
|
override val height: Int;
|
||||||
|
@ -36,11 +38,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
val canMerge: Boolean;
|
val canMerge: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||||
|
@ -57,17 +62,30 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||||
return manifest;
|
return manifest;
|
||||||
if(_obj.isClosed)
|
if(_obj.isClosed)
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient) {
|
if(_plugin is DevJSClient) {
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,12 +118,16 @@ class JSDashManifestMergingRawSource(
|
||||||
if(videoDash == null) return null;
|
if(videoDash == null) return null;
|
||||||
|
|
||||||
//TODO: Temporary simple solution..make more reliable version
|
//TODO: Temporary simple solution..make more reliable version
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||||
if(audioAdaptationSet != null) {
|
if(audioAdaptationSet != null) {
|
||||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return videoDash;
|
result = videoDash;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -21,6 +21,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
||||||
val contextName = "HLSAudioSource";
|
val contextName = "HLSAudioSource";
|
||||||
|
@ -32,6 +33,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
language = _obj.getOrThrow(config, "language", contextName);
|
language = _obj.getOrThrow(config, "language", contextName);
|
||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ class StateCasting {
|
||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer();
|
||||||
private var _started = false;
|
private var _started = false;
|
||||||
|
|
||||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||||
|
|
|
@ -22,7 +22,6 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
@ -30,6 +29,7 @@ import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.ClaimType
|
import com.futo.polycentric.core.ClaimType
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
|
@ -141,11 +141,17 @@ class VideoDownload {
|
||||||
var error: String? = null;
|
var error: String? = null;
|
||||||
|
|
||||||
var videoFilePath: String? = null;
|
var videoFilePath: String? = null;
|
||||||
var videoFileName: String? = null;
|
var videoFileNameBase: String? = null;
|
||||||
|
var videoFileNameExt: String? = null;
|
||||||
|
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
|
||||||
|
var videoOverrideContainer: String? = null;
|
||||||
var videoFileSize: Long? = null;
|
var videoFileSize: Long? = null;
|
||||||
|
|
||||||
var audioFilePath: String? = null;
|
var audioFilePath: String? = null;
|
||||||
var audioFileName: String? = null;
|
var audioFileNameBase: String? = null;
|
||||||
|
var audioFileNameExt: String? = null;
|
||||||
|
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
|
||||||
|
var audioOverrideContainer: String? = null;
|
||||||
var audioFileSize: Long? = null;
|
var audioFileSize: Long? = null;
|
||||||
|
|
||||||
var subtitleFilePath: String? = null;
|
var subtitleFilePath: String? = null;
|
||||||
|
@ -235,11 +241,13 @@ class VideoDownload {
|
||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
videoOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
audioOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(video == null && videoDetails == null)
|
if(video == null && videoDetails == null)
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
|
@ -367,8 +375,8 @@ class VideoDownload {
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
|
|
||||||
if(asource is JSSource) {
|
if(asource is JSSource) {
|
||||||
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || asource.hasRequestExecutor;
|
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(asource == null) {
|
if(asource == null) {
|
||||||
|
@ -410,11 +418,13 @@ class VideoDownload {
|
||||||
else audioSource;
|
else audioSource;
|
||||||
|
|
||||||
if(actualVideoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||||
|
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(actualAudioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||||
|
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
|
@ -1058,8 +1068,8 @@ class VideoDownload {
|
||||||
fun complete() {
|
fun complete() {
|
||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
|
@ -1140,7 +1150,7 @@ class VideoDownload {
|
||||||
else if (container.contains("video/x-matroska"))
|
else if (container.contains("video/x-matroska"))
|
||||||
return "mkv";
|
return "mkv";
|
||||||
else
|
else
|
||||||
return "video";
|
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
|
@ -1151,11 +1161,11 @@ class VideoDownload {
|
||||||
else if (container.contains("audio/mp3"))
|
else if (container.contains("audio/mp3"))
|
||||||
return "mp3";
|
return "mp3";
|
||||||
else if (container.contains("audio/webm"))
|
else if (container.contains("audio/webm"))
|
||||||
return "webma";
|
return "webm";
|
||||||
else if (container == "application/vnd.apple.mpegurl")
|
else if (container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "mp4a";
|
||||||
else
|
else
|
||||||
return "audio";
|
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subtitleContainerToExtension(container: String?): String {
|
fun subtitleContainerToExtension(container: String?): String {
|
||||||
|
|
|
@ -39,7 +39,7 @@ class VideoExport {
|
||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
|
||||||
val v = videoSource;
|
val v = videoSource;
|
||||||
val a = audioSource;
|
val a = audioSource;
|
||||||
val s = subtitleSource;
|
val s = subtitleSource;
|
||||||
|
@ -50,7 +50,7 @@ class VideoExport {
|
||||||
if (s != null) sourceCount++;
|
if (s != null) sourceCount++;
|
||||||
|
|
||||||
val outputFile: DocumentFile?;
|
val outputFile: DocumentFile?;
|
||||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||||
if (sourceCount > 1) {
|
if (sourceCount > 1) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||||
|
|
|
@ -13,7 +13,6 @@ import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
|
@ -21,6 +20,7 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanNumber
|
import com.futo.platformplayer.toHumanNumber
|
||||||
import com.futo.platformplayer.views.platform.PlatformLinkView
|
import com.futo.platformplayer.views.platform.PlatformLinkView
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.toName
|
import com.futo.polycentric.core.toName
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
|
||||||
|
@ -134,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!map.containsKey("Harbor"))
|
if(!map.containsKey("Harbor"))
|
||||||
this.context?.let {
|
map.set("Harbor", polycentricProfile.getHarborUrl());
|
||||||
map.set("Harbor", polycentricProfile.getHarborUrl(it));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (map.isNotEmpty())
|
if (map.isNotEmpty())
|
||||||
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
||||||
|
|
|
@ -29,7 +29,6 @@ import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -39,6 +38,7 @@ import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
|
@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
class ChannelListFragment : Fragment, IChannelTabFragment {
|
class ChannelListFragment : Fragment, IChannelTabFragment {
|
||||||
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
|
|
|
@ -8,8 +8,8 @@ import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.views.SupportView
|
import com.futo.platformplayer.views.SupportView
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
|
|
||||||
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
interface IChannelTabFragment {
|
interface IChannelTabFragment {
|
||||||
fun setChannel(channel: IPlatformChannel)
|
fun setChannel(channel: IPlatformChannel)
|
||||||
|
|
|
@ -42,7 +42,6 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.selectHighestResolutionImage
|
import com.futo.platformplayer.selectHighestResolutionImage
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -55,29 +54,14 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.polycentric.core.OwnedClaim
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.Store
|
|
||||||
import com.futo.polycentric.core.SystemState
|
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PolycentricProfile(
|
|
||||||
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
|
|
||||||
) {
|
|
||||||
fun getHarborUrl(context: Context): String{
|
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system));
|
|
||||||
val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
|
||||||
return "https://harbor.social/" + url.substring("polycentric://".length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChannelFragment : MainFragment() {
|
class ChannelFragment : MainFragment() {
|
||||||
override val isMainView: Boolean = true
|
override val isMainView: Boolean = true
|
||||||
|
@ -144,15 +128,14 @@ class ChannelFragment : MainFragment() {
|
||||||
|
|
||||||
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>
|
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricProfile?>
|
||||||
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
|
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
inflater.inflate(R.layout.fragment_channel, this)
|
inflater.inflate(R.layout.fragment_channel, this)
|
||||||
_taskLoadPolycentricProfile =
|
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>({ fragment.lifecycleScope },
|
||||||
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
|
|
||||||
{ id ->
|
{ id ->
|
||||||
return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
|
return@TaskHandler ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, id.claimFieldType.toLong(), id.claimType.toLong(), id.value!!)
|
||||||
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
|
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load polycentric profile.", it)
|
Logger.w(TAG, "Failed to load polycentric profile.", it)
|
||||||
}
|
}
|
||||||
|
@ -238,8 +221,8 @@ class ChannelFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
if (content is IPlatformVideo) {
|
if (content is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.onUrlClicked.subscribe { url ->
|
adapter.onUrlClicked.subscribe { url ->
|
||||||
|
@ -328,7 +311,7 @@ class ChannelFragment : MainFragment() {
|
||||||
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
||||||
Glide.with(_imageBanner).clear(_imageBanner)
|
Glide.with(_imageBanner).clear(_imageBanner)
|
||||||
|
|
||||||
loadPolycentricProfile(parameter.id, parameter.url)
|
loadPolycentricProfile(parameter.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.url
|
_url = parameter.url
|
||||||
|
@ -342,7 +325,7 @@ class ChannelFragment : MainFragment() {
|
||||||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
||||||
Glide.with(_imageBanner).clear(_imageBanner)
|
Glide.with(_imageBanner).clear(_imageBanner)
|
||||||
|
|
||||||
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
|
loadPolycentricProfile(parameter.channel.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.channel.url
|
_url = parameter.channel.url
|
||||||
|
@ -359,16 +342,8 @@ class ChannelFragment : MainFragment() {
|
||||||
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPolycentricProfile(id: PlatformID, url: String) {
|
private fun loadPolycentricProfile(id: PlatformID) {
|
||||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
|
_taskLoadPolycentricProfile.run(id)
|
||||||
if (cachedPolycentricProfile != null) {
|
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = true)
|
|
||||||
if (cachedPolycentricProfile.expired) {
|
|
||||||
_taskLoadPolycentricProfile.run(id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadPolycentricProfile.run(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLoading(isLoading: Boolean) {
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
@ -533,20 +508,13 @@ class ChannelFragment : MainFragment() {
|
||||||
|
|
||||||
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||||
setPolycentricProfile(null, animate = false)
|
setPolycentricProfile(null, animate = false)
|
||||||
|
or()
|
||||||
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
setPolycentricProfile(cachedProfile, animate = false)
|
|
||||||
} else {
|
|
||||||
or()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(
|
private fun setPolycentricProfile(
|
||||||
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
|
profile: PolycentricProfile?, animate: Boolean
|
||||||
) {
|
) {
|
||||||
val dp35 = 35.dp(resources)
|
val dp35 = 35.dp(resources)
|
||||||
val profile = cachedPolycentricProfile?.profile
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
||||||
it.toURLInfoSystemLinkUrl(
|
it.toURLInfoSystemLinkUrl(
|
||||||
profile.system.toProto(), it.process, profile.systemState.servers.toList()
|
profile.system.toProto(), it.process, profile.systemState.servers.toList()
|
||||||
|
|
|
@ -23,7 +23,6 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -32,6 +31,7 @@ import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
|
@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||||
};
|
};
|
||||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
adapter.onLongPress.subscribe(this) {
|
adapter.onLongPress.subscribe(this) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -26,6 +27,7 @@ class CreatorsFragment : MainFragment() {
|
||||||
private var _overlayContainer: FrameLayout? = null;
|
private var _overlayContainer: FrameLayout? = null;
|
||||||
private var _containerSearch: FrameLayout? = null;
|
private var _containerSearch: FrameLayout? = null;
|
||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
|
private var _textMeta: TextView? = null;
|
||||||
private var _buttonClearSearch: ImageButton? = null
|
private var _buttonClearSearch: ImageButton? = null
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
@ -34,6 +36,7 @@ class CreatorsFragment : MainFragment() {
|
||||||
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||||
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||||
_editSearch = editSearch
|
_editSearch = editSearch
|
||||||
|
_textMeta = view.findViewById(R.id.text_meta);
|
||||||
_buttonClearSearch = buttonClearSearch
|
_buttonClearSearch = buttonClearSearch
|
||||||
buttonClearSearch.setOnClickListener {
|
buttonClearSearch.setOnClickListener {
|
||||||
editSearch.text.clear()
|
editSearch.text.clear()
|
||||||
|
@ -41,7 +44,11 @@ class CreatorsFragment : MainFragment() {
|
||||||
_buttonClearSearch?.visibility = View.INVISIBLE;
|
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
|
||||||
|
_textMeta?.let {
|
||||||
|
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
||||||
|
}
|
||||||
|
};
|
||||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||||
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.toHumanBytesSize
|
import com.futo.platformplayer.toHumanBytesSize
|
||||||
|
import com.futo.platformplayer.toHumanDuration
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
||||||
|
@ -215,7 +216,7 @@ class DownloadsFragment : MainFragment() {
|
||||||
_listDownloadedHeader.visibility = GONE;
|
_listDownloadedHeader.visibility = GONE;
|
||||||
} else {
|
} else {
|
||||||
_listDownloadedHeader.visibility = VISIBLE;
|
_listDownloadedHeader.visibility = VISIBLE;
|
||||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})";
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDownloads = downloaded;
|
lastDownloads = downloaded;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.NoResultsView
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
|
import com.futo.platformplayer.views.ToggleBar
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
|
@ -94,6 +95,8 @@ class HomeFragment : MainFragment() {
|
||||||
class HomeView : ContentFeedView<HomeFragment> {
|
class HomeView : ContentFeedView<HomeFragment> {
|
||||||
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
||||||
|
|
||||||
|
private var _toggleBar: ToggleBar? = null;
|
||||||
|
|
||||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||||
|
|
||||||
|
@ -127,6 +130,8 @@ class HomeFragment : MainFragment() {
|
||||||
}, fragment);
|
}, fragment);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
initializeToolbarContent();
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
showAnnouncementView()
|
showAnnouncementView()
|
||||||
}
|
}
|
||||||
|
@ -201,13 +206,43 @@ class HomeFragment : MainFragment() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
private val _filterLock = Object();
|
||||||
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
private var _toggleRecent = false;
|
||||||
|
fun initializeToolbarContent() {
|
||||||
|
//Not stable enough with current viewport paging, doesn't work with less results, and reloads content instead of just re-filtering existing
|
||||||
|
/*
|
||||||
|
_toggleBar = ToggleBar(context).apply {
|
||||||
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||||
|
}
|
||||||
|
synchronized(_filterLock) {
|
||||||
|
_toggleBar?.setToggles(
|
||||||
|
//TODO: loadResults needs to be replaced with an internal reload of the current content
|
||||||
|
ToggleBar.Toggle("Recent", _toggleRecent) { _toggleRecent = it; loadResults(false) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(_toggleBar, 0);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadResults() {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
|
return results.filter {
|
||||||
|
if(StateMeta.instance.isVideoHidden(it.url))
|
||||||
|
return@filter false;
|
||||||
|
if(StateMeta.instance.isCreatorHidden(it.author.url))
|
||||||
|
return@filter false;
|
||||||
|
|
||||||
|
if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 23) {
|
||||||
|
return@filter false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@filter true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadResults(withRefetch: Boolean = true) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
_taskGetPager.run(true);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||||
if (pager is EmptyPager<IPlatformContent>) {
|
if (pager is EmptyPager<IPlatformContent>) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.view.ViewGroup
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
@ -78,6 +79,14 @@ class PlaylistFragment : MainFragment() {
|
||||||
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||||
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
_buttonExport.setOnClickListener {
|
||||||
|
_playlist?.let {
|
||||||
|
val context = StateApp.instance.contextOrNull ?: return@let;
|
||||||
|
if(context is IWithResultLauncher)
|
||||||
|
StateDownloads.instance.exportPlaylist(context, it.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_buttonDownload.visibility = View.VISIBLE;
|
_buttonDownload.visibility = View.VISIBLE;
|
||||||
editPlaylistOverlay.onOK.subscribe {
|
editPlaylistOverlay.onOK.subscribe {
|
||||||
val text = nameInput.text;
|
val text = nameInput.text;
|
||||||
|
@ -176,6 +185,7 @@ class PlaylistFragment : MainFragment() {
|
||||||
setVideos(parameter.videos, true)
|
setVideos(parameter.videos, true)
|
||||||
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
||||||
setButtonDownloadVisible(true)
|
setButtonDownloadVisible(true)
|
||||||
|
setButtonExportVisible(false)
|
||||||
setButtonEditVisible(true)
|
setButtonEditVisible(true)
|
||||||
|
|
||||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||||
|
|
|
@ -33,10 +33,8 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -47,7 +45,6 @@ import com.futo.platformplayer.views.adapters.ChannelTab
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.others.Toggle
|
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
@ -57,6 +54,8 @@ import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Models
|
import com.futo.polycentric.core.Models
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.google.android.flexbox.FlexboxLayout
|
import com.google.android.flexbox.FlexboxLayout
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.shape.CornerFamily
|
import com.google.android.material.shape.CornerFamily
|
||||||
|
@ -112,7 +111,7 @@ class PostDetailFragment : MainFragment {
|
||||||
private var _isLoading = false;
|
private var _isLoading = false;
|
||||||
private var _post: IPlatformPostDetails? = null;
|
private var _post: IPlatformPostDetails? = null;
|
||||||
private var _postOverview: IPlatformPost? = null;
|
private var _postOverview: IPlatformPost? = null;
|
||||||
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
private var _polycentricProfile: PolycentricProfile? = null;
|
||||||
private var _version = 0;
|
private var _version = 0;
|
||||||
private var _isRepliesVisible: Boolean = false;
|
private var _isRepliesVisible: Boolean = false;
|
||||||
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
||||||
|
@ -169,7 +168,7 @@ class PostDetailFragment : MainFragment {
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
|
||||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
Logger.w(TAG, "Failed to load claims.", it);
|
||||||
|
@ -274,7 +273,7 @@ class PostDetailFragment : MainFragment {
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonStore.setOnClickListener {
|
_buttonStore.setOnClickListener {
|
||||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
_polycentricProfile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
|
@ -334,7 +333,7 @@ class PostDetailFragment : MainFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||||
ContentType.OPINION.value).setValue(
|
ContentType.OPINION.value).setValue(
|
||||||
|
@ -604,16 +603,8 @@ class PostDetailFragment : MainFragment {
|
||||||
|
|
||||||
private fun fetchPolycentricProfile() {
|
private fun fetchPolycentricProfile() {
|
||||||
val author = _post?.author ?: _postOverview?.author ?: return;
|
val author = _post?.author ?: _postOverview?.author ?: return;
|
||||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true);
|
|
||||||
if (cachedPolycentricProfile != null) {
|
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
|
||||||
if (cachedPolycentricProfile.expired) {
|
|
||||||
_taskLoadPolycentricProfile.run(author.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setPolycentricProfile(null, animate = false);
|
setPolycentricProfile(null, animate = false);
|
||||||
_taskLoadPolycentricProfile.run(author.id);
|
_taskLoadPolycentricProfile.run(author.id);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setChannelMeta(value: IPlatformPost?) {
|
private fun setChannelMeta(value: IPlatformPost?) {
|
||||||
|
@ -639,17 +630,18 @@ class PostDetailFragment : MainFragment {
|
||||||
_repliesOverlay.cleanup();
|
_repliesOverlay.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = cachedPolycentricProfile;
|
_polycentricProfile = polycentricProfile;
|
||||||
|
|
||||||
if (cachedPolycentricProfile?.profile == null) {
|
val pp = _polycentricProfile;
|
||||||
|
if (pp == null) {
|
||||||
_layoutMonetization.visibility = View.GONE;
|
_layoutMonetization.visibility = View.GONE;
|
||||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.VISIBLE;
|
_layoutMonetization.visibility = View.VISIBLE;
|
||||||
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
_creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPost() {
|
private fun fetchPost() {
|
||||||
|
|
|
@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
|
||||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||||
|
|
||||||
val config = SourcePluginConfig.fromJson(configJson);
|
val config = SourcePluginConfig.fromJson(configJson);
|
||||||
if (config.version <= c.version && config.name != "Youtube") {
|
if (config.version <= c.version) {
|
||||||
Logger.i(TAG, "Plugin is up to date.");
|
Logger.i(TAG, "Plugin is up to date.");
|
||||||
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
||||||
return@launch;
|
return@launch;
|
||||||
|
|
|
@ -256,6 +256,8 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||||
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
||||||
if(sub != null && sub.channel.thumbnail != null) {
|
if(sub != null && sub.channel.thumbnail != null) {
|
||||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||||
|
if(g.image != null)
|
||||||
|
g.image!!.subscriptionUrl = sub.channel.url;
|
||||||
g.image?.setImageView(_imageGroup);
|
g.image?.setImageView(_imageGroup);
|
||||||
g.image?.setImageView(_imageGroupBackground);
|
g.image?.setImageView(_imageGroupBackground);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -94,12 +94,10 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.getNowDiffSeconds
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
|
@ -158,6 +156,8 @@ import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Models
|
import com.futo.polycentric.core.Models
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -294,7 +294,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
private set;
|
private set;
|
||||||
private var _historicalPosition: Long = 0;
|
private var _historicalPosition: Long = 0;
|
||||||
private var _commentsCount = 0;
|
private var _commentsCount = 0;
|
||||||
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
private var _polycentricProfile: PolycentricProfile? = null;
|
||||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||||
private var _autoplayVideo: IPlatformVideo? = null
|
private var _autoplayVideo: IPlatformVideo? = null
|
||||||
|
|
||||||
|
@ -409,12 +409,12 @@ class VideoDetailView : ConstraintLayout {
|
||||||
};
|
};
|
||||||
|
|
||||||
_monetization.onSupportTap.subscribe {
|
_monetization.onSupportTap.subscribe {
|
||||||
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile);
|
_container_content_support.setPolycentricProfile(_polycentricProfile);
|
||||||
switchContentView(_container_content_support);
|
switchContentView(_container_content_support);
|
||||||
};
|
};
|
||||||
|
|
||||||
_monetization.onStoreTap.subscribe {
|
_monetization.onStoreTap.subscribe {
|
||||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
_polycentricProfile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
|
@ -579,6 +579,14 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_minimize_title.setOnClickListener { onMaximize.emit(false) };
|
_minimize_title.setOnClickListener { onMaximize.emit(false) };
|
||||||
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
|
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
|
||||||
|
|
||||||
|
_player.onStateChange.subscribe {
|
||||||
|
if (_player.activelyPlaying) {
|
||||||
|
Logger.i(TAG, "Play changed, resetting error counter _didTriggerDatasourceErrorCount = 0 (_player.activelyPlaying: ${_player.activelyPlaying})")
|
||||||
|
_didTriggerDatasourceErrorCount = 0;
|
||||||
|
_didTriggerDatasourceError = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_player.onPlayChanged.subscribe {
|
_player.onPlayChanged.subscribe {
|
||||||
if (StateCasting.instance.activeDevice == null) {
|
if (StateCasting.instance.activeDevice == null) {
|
||||||
handlePlayChanged(it);
|
handlePlayChanged(it);
|
||||||
|
@ -882,7 +890,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
}
|
}
|
||||||
else null,
|
else null,
|
||||||
if(!isLimitedVersion)
|
if(!isLimitedVersion && !(video?.isLive ?: false))
|
||||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||||
video?.let {
|
video?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||||
|
@ -922,7 +930,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
} else if(devices.size == 1){
|
} else if(devices.size == 1){
|
||||||
val device = devices.first();
|
val device = devices.first();
|
||||||
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||||
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , {
|
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non '${device.displayName}'" , {
|
||||||
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -963,6 +971,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
_videoResumePositionMilliseconds = _player.position
|
||||||
setVideoDetails(video);
|
setVideoDetails(video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1227,16 +1236,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||||
_channelName.text = video.author.name;
|
_channelName.text = video.author.name;
|
||||||
|
|
||||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
setPolycentricProfile(null, animate = false);
|
||||||
if (cachedPolycentricProfile != null) {
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
|
||||||
if (cachedPolycentricProfile.expired) {
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setPolycentricProfile(null, animate = false);
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_player.clear();
|
_player.clear();
|
||||||
|
|
||||||
|
@ -1265,8 +1266,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||||
_didTriggerDatasourceErrroCount = 0;
|
|
||||||
_didTriggerDatasourceError = false;
|
|
||||||
_autoplayVideo = null
|
_autoplayVideo = null
|
||||||
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
||||||
|
|
||||||
|
@ -1277,6 +1276,10 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_lastVideoSource = null;
|
_lastVideoSource = null;
|
||||||
_lastAudioSource = null;
|
_lastAudioSource = null;
|
||||||
_lastSubtitleSource = null;
|
_lastSubtitleSource = null;
|
||||||
|
|
||||||
|
Logger.i(TAG, "_didTriggerDatasourceErrorCount reset to 0 because new video")
|
||||||
|
_didTriggerDatasourceErrorCount = 0;
|
||||||
|
_didTriggerDatasourceError = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
||||||
|
@ -1394,11 +1397,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
setTabIndex(2, true)
|
setTabIndex(2, true)
|
||||||
} else {
|
} else {
|
||||||
when (Settings.instance.comments.defaultCommentSection) {
|
when (Settings.instance.comments.defaultCommentSection) {
|
||||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(
|
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
||||||
0,
|
1 -> setTabIndex(1, true)
|
||||||
true
|
|
||||||
) else setTabIndex(1, true);
|
|
||||||
1 -> setTabIndex(1, true);
|
|
||||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1436,16 +1436,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
||||||
setDescription(video.description.fixHtmlLinks());
|
setDescription(video.description.fixHtmlLinks());
|
||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||||
|
setPolycentricProfile(null, animate = false);
|
||||||
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
val cachedPolycentricProfile =
|
|
||||||
PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
|
||||||
if (cachedPolycentricProfile != null) {
|
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
|
||||||
} else {
|
|
||||||
setPolycentricProfile(null, animate = false);
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_platform.setPlatformFromClientID(video.id.pluginId);
|
_platform.setPlatformFromClientID(video.id.pluginId);
|
||||||
val subTitleSegments: ArrayList<String> = ArrayList();
|
val subTitleSegments: ArrayList<String> = ArrayList();
|
||||||
|
@ -1474,7 +1466,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
||||||
PolycentricCache.SERVER, ref, null, null,
|
ApiMethods.SERVER, ref, null, null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
.setFromType(ContentType.OPINION.value).setValue(
|
.setFromType(ContentType.OPINION.value).setValue(
|
||||||
|
@ -1490,10 +1482,8 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
val likes = queryReferencesResponse.countsList[0];
|
val likes = queryReferencesResponse.countsList[0];
|
||||||
val dislikes = queryReferencesResponse.countsList[1];
|
val dislikes = queryReferencesResponse.countsList[1];
|
||||||
val hasLiked =
|
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||||
StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||||
val hasDisliked =
|
|
||||||
StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_rating.visibility = View.VISIBLE;
|
_rating.visibility = View.VISIBLE;
|
||||||
|
@ -1831,7 +1821,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _didTriggerDatasourceErrroCount = 0;
|
private var _didTriggerDatasourceErrorCount = 0;
|
||||||
private var _didTriggerDatasourceError = false;
|
private var _didTriggerDatasourceError = false;
|
||||||
private fun onDataSourceError(exception: Throwable) {
|
private fun onDataSourceError(exception: Throwable) {
|
||||||
Logger.e(TAG, "onDataSourceError", exception);
|
Logger.e(TAG, "onDataSourceError", exception);
|
||||||
|
@ -1841,32 +1831,53 @@ class VideoDetailView : ConstraintLayout {
|
||||||
return;
|
return;
|
||||||
val config = currentVideo.sourceConfig;
|
val config = currentVideo.sourceConfig;
|
||||||
|
|
||||||
if(_didTriggerDatasourceErrroCount <= 3) {
|
if(_didTriggerDatasourceErrorCount <= 3) {
|
||||||
_didTriggerDatasourceError = true;
|
_didTriggerDatasourceError = true;
|
||||||
_didTriggerDatasourceErrroCount++;
|
_didTriggerDatasourceErrorCount++;
|
||||||
|
|
||||||
|
UIDialogs.toast("Detected video error, attempting automatic reload (${_didTriggerDatasourceErrorCount})");
|
||||||
|
Logger.i(TAG, "Block detected, attempting bypass (_didTriggerDatasourceErrorCount = ${_didTriggerDatasourceErrorCount})");
|
||||||
|
|
||||||
UIDialogs.toast("Block detected, attempting bypass");
|
|
||||||
//return;
|
//return;
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
try {
|
||||||
val previousVideoSource = _lastVideoSource;
|
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||||
val previousAudioSource = _lastAudioSource;
|
val previousVideoSource = _lastVideoSource;
|
||||||
|
val previousAudioSource = _lastAudioSource;
|
||||||
|
|
||||||
if(newDetails is IPlatformVideoDetails) {
|
if (newDetails is IPlatformVideoDetails) {
|
||||||
val newVideoSource = if(previousVideoSource != null)
|
val newVideoSource = if (previousVideoSource != null)
|
||||||
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
|
VideoHelper.selectBestVideoSource(
|
||||||
else null;
|
newDetails.video,
|
||||||
val newAudioSource = if(previousAudioSource != null)
|
previousVideoSource.height * previousVideoSource.width,
|
||||||
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
else null;
|
);
|
||||||
withContext(Dispatchers.Main) {
|
else null;
|
||||||
video = newDetails;
|
val newAudioSource = if (previousAudioSource != null)
|
||||||
_player.setSource(newVideoSource, newAudioSource, true, true);
|
VideoHelper.selectBestAudioSource(
|
||||||
|
newDetails.video,
|
||||||
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
|
previousAudioSource.language,
|
||||||
|
previousAudioSource.bitrate.toLong()
|
||||||
|
);
|
||||||
|
else null;
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
video = newDetails;
|
||||||
|
_player.setSource(newVideoSource, newAudioSource, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to get video details, attempting retrying without reloading.", e)
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
video?.let {
|
||||||
|
_videoResumePositionMilliseconds = _player.position
|
||||||
|
setVideoDetails(it, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(_didTriggerDatasourceErrroCount > 3) {
|
else if(_didTriggerDatasourceErrorCount > 3) {
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||||
context.getString(R.string.media_error),
|
context.getString(R.string.media_error),
|
||||||
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
||||||
|
@ -2595,8 +2606,13 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
onAddToWatchLaterClicked.subscribe(this) {
|
onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onAddToQueueClicked.subscribe(this) {
|
||||||
|
if(it is IPlatformVideo) {
|
||||||
|
StatePlayer.instance.addToQueue(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2768,13 +2784,12 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = cachedPolycentricProfile;
|
_polycentricProfile = profile
|
||||||
|
|
||||||
val dp_35 = 35.dp(context.resources)
|
val dp_35 = 35.dp(context.resources)
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
|
||||||
|
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
|
@ -2783,12 +2798,12 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
val username = profile?.systemState?.username
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
_channelName.text = username
|
_channelName.text = username
|
||||||
}
|
}
|
||||||
|
|
||||||
_monetization.setPolycentricProfile(cachedPolycentricProfile);
|
_monetization.setPolycentricProfile(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||||
|
@ -2976,7 +2991,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
Logger.w(TAG, "Failed to load recommendations.", it);
|
Logger.w(TAG, "Failed to load recommendations.", it);
|
||||||
};
|
};
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
|
||||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
Logger.w(TAG, "Failed to load claims.", it);
|
||||||
|
|
|
@ -34,6 +34,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
protected var overlayContainer: FrameLayout
|
protected var overlayContainer: FrameLayout
|
||||||
private set;
|
private set;
|
||||||
protected var _buttonDownload: ImageButton;
|
protected var _buttonDownload: ImageButton;
|
||||||
|
protected var _buttonExport: ImageButton;
|
||||||
private var _buttonShare: ImageButton;
|
private var _buttonShare: ImageButton;
|
||||||
private var _buttonEdit: ImageButton;
|
private var _buttonEdit: ImageButton;
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
_buttonEdit = findViewById(R.id.button_edit);
|
_buttonEdit = findViewById(R.id.button_edit);
|
||||||
_buttonDownload = findViewById(R.id.button_download);
|
_buttonDownload = findViewById(R.id.button_download);
|
||||||
_buttonDownload.visibility = View.GONE;
|
_buttonDownload.visibility = View.GONE;
|
||||||
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
|
_buttonExport.visibility = View.GONE;
|
||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share);
|
_buttonShare = findViewById(R.id.button_share);
|
||||||
val onShare = _onShare;
|
val onShare = _onShare;
|
||||||
|
@ -68,6 +71,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||||
|
|
||||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||||
|
setButtonExportVisible(false);
|
||||||
setButtonDownloadVisible(canEdit());
|
setButtonDownloadVisible(canEdit());
|
||||||
|
|
||||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||||
|
@ -108,6 +112,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||||
|
|
||||||
if(isDownloading) {
|
if(isDownloading) {
|
||||||
|
setButtonExportVisible(false);
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
|
@ -117,6 +122,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(isDownloaded) {
|
else if(isDownloaded) {
|
||||||
|
setButtonExportVisible(true)
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
|
@ -125,6 +131,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
setButtonExportVisible(false);
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
onDownload();
|
onDownload();
|
||||||
|
@ -171,6 +178,9 @@ abstract class VideoListEditorView : LinearLayout {
|
||||||
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
||||||
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||||
}
|
}
|
||||||
|
protected fun setButtonExportVisible(isVisible: Boolean) {
|
||||||
|
_buttonExport.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
protected fun setButtonEditVisible(isVisible: Boolean) {
|
protected fun setButtonEditVisible(isVisible: Boolean) {
|
||||||
_buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
_buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||||
|
|
|
@ -14,9 +14,9 @@ import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.views.casting.CastButton
|
import com.futo.platformplayer.views.casting.CastButton
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
class NavigationTopBarFragment : TopFragment() {
|
class NavigationTopBarFragment : TopFragment() {
|
||||||
private var _buttonBack: ImageButton? = null;
|
private var _buttonBack: ImageButton? = null;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.media3.datasource.ResolvingDataSource
|
||||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
@ -85,12 +86,17 @@ class VideoHelper {
|
||||||
|
|
||||||
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
|
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
|
||||||
}
|
}
|
||||||
fun selectBestAudioSource(altSources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
fun selectBestAudioSource(sources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
||||||
|
val hasPriority = sources.any { it.priority };
|
||||||
|
var altSources = if(hasPriority) sources.filter { it.priority } else sources;
|
||||||
|
val hasOriginal = altSources.any { it.original };
|
||||||
|
if(hasOriginal && Settings.instance.playback.preferOriginalAudio)
|
||||||
|
altSources = altSources.filter { it.original };
|
||||||
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
|
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
|
||||||
preferredLanguage
|
preferredLanguage
|
||||||
} else {
|
} else {
|
||||||
if(altSources.any { it.language == Language.ENGLISH })
|
if(altSources.any { it.language == Language.ENGLISH })
|
||||||
Language.ENGLISH
|
Language.ENGLISH;
|
||||||
else
|
else
|
||||||
Language.UNKNOWN;
|
Language.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.futo.platformplayer.images;
|
package com.futo.platformplayer.images;
|
||||||
|
|
||||||
|
import static com.futo.platformplayer.Extensions_PolycentricKt.getDataLinkFromUrl;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -12,10 +14,14 @@ import com.bumptech.glide.load.model.ModelLoader;
|
||||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache;
|
import com.futo.polycentric.core.ApiMethods;
|
||||||
|
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
|
import kotlinx.coroutines.CoroutineScopeKt;
|
||||||
import kotlinx.coroutines.Deferred;
|
import kotlinx.coroutines.Deferred;
|
||||||
|
import kotlinx.coroutines.Dispatchers;
|
||||||
|
import userpackage.Protocol;
|
||||||
|
|
||||||
import java.lang.Exception;
|
import java.lang.Exception;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
@ -60,7 +66,14 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
|
||||||
@Override
|
@Override
|
||||||
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
||||||
Log.i("PolycentricModelLoader", this._model);
|
Log.i("PolycentricModelLoader", this._model);
|
||||||
_deferred = PolycentricCache.getInstance().getDataAsync(_model);
|
|
||||||
|
Protocol.URLInfoDataLink dataLink = getDataLinkFromUrl(_model);
|
||||||
|
if (dataLink == null) {
|
||||||
|
callback.onLoadFailed(new Exception("Data link cannot be null"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deferred = ApiMethods.Companion.getDataFromServerAndReassemble(CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()), dataLink);
|
||||||
_deferred.invokeOnCompletion(throwable -> {
|
_deferred.invokeOnCompletion(throwable -> {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
|
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.widget.ImageView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.PresetImages
|
import com.futo.platformplayer.PresetImages
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -18,7 +19,8 @@ data class ImageVariable(
|
||||||
@Transient
|
@Transient
|
||||||
@Contextual
|
@Contextual
|
||||||
private val bitmap: Bitmap? = null,
|
private val bitmap: Bitmap? = null,
|
||||||
val presetName: String? = null) {
|
val presetName: String? = null,
|
||||||
|
var subscriptionUrl: String? = null) {
|
||||||
|
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
||||||
|
@ -63,7 +65,13 @@ data class ImageVariable(
|
||||||
return ImageVariable(null, null, null, str);
|
return ImageVariable(null, null, null, str);
|
||||||
}
|
}
|
||||||
fun fromFile(file: File): ImageVariable {
|
fun fromFile(file: File): ImageVariable {
|
||||||
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
try {
|
||||||
|
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("ImageVariable", "Unsupported image format? " + ex.message, ex);
|
||||||
|
return fromResource(R.drawable.ic_error_pred);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -125,7 +125,7 @@ class HLS {
|
||||||
return if (source is IHLSManifestSource) {
|
return if (source is IHLSManifestSource) {
|
||||||
listOf()
|
listOf()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url))
|
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, url))
|
||||||
} else {
|
} else {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
@ -346,7 +346,7 @@ class HLS {
|
||||||
|
|
||||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||||
return@mapNotNull when (it.type) {
|
return@mapNotNull when (it.type) {
|
||||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, false, it.uri)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
|
@ -50,14 +49,7 @@ class StateCache {
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||||
val allUrls = subs
|
val allUrls = subs
|
||||||
.map {
|
.map { it.channel.url }
|
||||||
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
|
||||||
if(!otherUrls.contains(it.channel.url))
|
|
||||||
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
|
|
||||||
else
|
|
||||||
return@map otherUrls;
|
|
||||||
}
|
|
||||||
.flatten()
|
|
||||||
.distinct()
|
.distinct()
|
||||||
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,11 @@ package com.futo.platformplayer.states
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
@ -46,6 +48,17 @@ class StateDownloads {
|
||||||
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
|
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
|
||||||
|
|
||||||
private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded")
|
private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded")
|
||||||
|
.withOnModified({
|
||||||
|
synchronized(_downloadedSet) {
|
||||||
|
if(!_downloadedSet.contains(it.id))
|
||||||
|
_downloadedSet.add(it.id);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
synchronized(_downloadedSet) {
|
||||||
|
if(_downloadedSet.contains(it.id))
|
||||||
|
_downloadedSet.remove(it.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
.load()
|
.load()
|
||||||
.apply { afterLoadingDownloaded(this) };
|
.apply { afterLoadingDownloaded(this) };
|
||||||
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
|
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
|
||||||
|
@ -85,9 +98,6 @@ class StateDownloads {
|
||||||
Logger.i("StateDownloads", "Deleting local video ${id.value}");
|
Logger.i("StateDownloads", "Deleting local video ${id.value}");
|
||||||
val downloaded = getCachedVideo(id);
|
val downloaded = getCachedVideo(id);
|
||||||
if(downloaded != null) {
|
if(downloaded != null) {
|
||||||
synchronized(_downloadedSet) {
|
|
||||||
_downloadedSet.remove(id);
|
|
||||||
}
|
|
||||||
_downloaded.delete(downloaded);
|
_downloaded.delete(downloaded);
|
||||||
}
|
}
|
||||||
onDownloadedChanged.emit();
|
onDownloadedChanged.emit();
|
||||||
|
@ -261,9 +271,6 @@ class StateDownloads {
|
||||||
if(existing.groupID == null) {
|
if(existing.groupID == null) {
|
||||||
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||||
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||||
synchronized(_downloadedSet) {
|
|
||||||
_downloadedSet.add(existing.id);
|
|
||||||
}
|
|
||||||
_downloaded.save(existing);
|
_downloaded.save(existing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,9 +313,6 @@ class StateDownloads {
|
||||||
if(existing.groupID == null) {
|
if(existing.groupID == null) {
|
||||||
existing.groupID = playlist.id;
|
existing.groupID = playlist.id;
|
||||||
existing.groupType = VideoDownload.GROUP_PLAYLIST;
|
existing.groupType = VideoDownload.GROUP_PLAYLIST;
|
||||||
synchronized(_downloadedSet) {
|
|
||||||
_downloadedSet.add(existing.id);
|
|
||||||
}
|
|
||||||
_downloaded.save(existing);
|
_downloaded.save(existing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,6 +470,65 @@ class StateDownloads {
|
||||||
return _downloadsDirectory;
|
return _downloadsDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun exportPlaylist(context: Context, playlistId: String) {
|
||||||
|
if(context is IWithResultLauncher)
|
||||||
|
StateApp.instance.requestDirectoryAccess(context, "Export Playlist", "To export playlist to directory", null) {
|
||||||
|
if (it == null)
|
||||||
|
return@requestDirectoryAccess;
|
||||||
|
|
||||||
|
val root = DocumentFile.fromTreeUri(context, it!!);
|
||||||
|
|
||||||
|
val playlist = StatePlaylists.instance.getPlaylist(playlistId);
|
||||||
|
var localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId);
|
||||||
|
if(playlist != null) {
|
||||||
|
val missing = playlist.videos
|
||||||
|
.filter { vid -> !localVideos.any { it.id.value == null || it.id.value == vid.id.value } }
|
||||||
|
.map { getCachedVideo(it.id) }
|
||||||
|
.filterNotNull();
|
||||||
|
if(missing.size > 0)
|
||||||
|
localVideos = localVideos + missing;
|
||||||
|
};
|
||||||
|
|
||||||
|
var lastNotifyTime = -1L;
|
||||||
|
|
||||||
|
UIDialogs.showDialogProgress(context) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
it.setText("Exporting videos..");
|
||||||
|
var i = 0;
|
||||||
|
var success = 0;
|
||||||
|
for (video in localVideos) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.setText("Exporting videos...(${i}/${localVideos.size})");
|
||||||
|
//it.setProgress(i.toDouble() / localVideos.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val export = VideoExport(video, video.videoSource.firstOrNull(), video.audioSource.firstOrNull(), video.subtitlesSources.firstOrNull());
|
||||||
|
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||||
|
|
||||||
|
val file = export.export(context, { progress ->
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||||
|
it.setProgress(progress);
|
||||||
|
lastNotifyTime = now;
|
||||||
|
}
|
||||||
|
}, root);
|
||||||
|
success++;
|
||||||
|
} catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.setProgress(1f);
|
||||||
|
it.dismiss();
|
||||||
|
UIDialogs.appToast("Finished exporting playlist (${success} videos${if(i < success) ", ${i} errors" else ""})");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||||
var lastNotifyTime = -1L;
|
var lastNotifyTime = -1L;
|
||||||
|
|
||||||
|
@ -477,13 +540,13 @@ class StateDownloads {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||||
|
|
||||||
val file = export.export(context) { progress ->
|
val file = export.export(context, { progress ->
|
||||||
val now = System.currentTimeMillis();
|
val now = System.currentTimeMillis();
|
||||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||||
it.setProgress(progress);
|
it.setProgress(progress);
|
||||||
lastNotifyTime = now;
|
lastNotifyTime = now;
|
||||||
}
|
}
|
||||||
}
|
}, null);
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
it.setProgress(100.0f)
|
it.setProgress(100.0f)
|
||||||
|
|
|
@ -14,7 +14,7 @@ class StateMeta {
|
||||||
return when(lastCommentSection.value){
|
return when(lastCommentSection.value){
|
||||||
"Polycentric" -> 0;
|
"Polycentric" -> 0;
|
||||||
"Platform" -> 1;
|
"Platform" -> 1;
|
||||||
else -> 1
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun setLastCommentSection(value: Int) {
|
fun setLastCommentSection(value: Int) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
@ -130,6 +131,12 @@ class StatePlayer {
|
||||||
closeMediaSession();
|
closeMediaSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveQueueAsPlaylist(name: String){
|
||||||
|
val videos = _queue.toList();
|
||||||
|
val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) });
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
}
|
||||||
|
|
||||||
//Notifications
|
//Notifications
|
||||||
fun hasMediaSession() : Boolean {
|
fun hasMediaSession() : Boolean {
|
||||||
return MediaPlaybackService.getService() != null;
|
return MediaPlaybackService.getService() != null;
|
||||||
|
|
|
@ -177,11 +177,14 @@ class StatePlaylists {
|
||||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1) {
|
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
|
||||||
|
var wasNew = false;
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
|
if(!_watchlistStore.hasItem { it.url == video.url })
|
||||||
|
wasNew = true;
|
||||||
_watchlistStore.saveAsync(video);
|
_watchlistStore.saveAsync(video);
|
||||||
if(orderPosition == -1)
|
if(orderPosition == -1)
|
||||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
|
||||||
else {
|
else {
|
||||||
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
||||||
existing.add(orderPosition, video.url);
|
existing.add(orderPosition, video.url);
|
||||||
|
@ -198,6 +201,7 @@ class StatePlaylists {
|
||||||
}
|
}
|
||||||
|
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
|
return wasNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastPlayedPlaylist() : Playlist? {
|
fun getLastPlayedPlaylist() : Playlist? {
|
||||||
|
@ -226,17 +230,20 @@ class StatePlaylists {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun getWatchLaterSyncPacket(orderOnly: Boolean = false): SyncWatchLaterPackage{
|
||||||
|
return SyncWatchLaterPackage(
|
||||||
|
if (orderOnly) listOf() else getWatchLater(),
|
||||||
|
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
||||||
|
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
||||||
|
getWatchLaterLastReorderTime().toEpochSecond(),
|
||||||
|
_watchlistOrderStore.values.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
private fun broadcastWatchLater(orderOnly: Boolean = false) {
|
private fun broadcastWatchLater(orderOnly: Boolean = false) {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.broadcastJsonData(
|
StateSync.instance.broadcastJsonData(
|
||||||
GJSyncOpcodes.syncWatchLater, SyncWatchLaterPackage(
|
GJSyncOpcodes.syncWatchLater, getWatchLaterSyncPacket(orderOnly)
|
||||||
if (orderOnly) listOf() else getWatchLater(),
|
|
||||||
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
|
||||||
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
|
||||||
getWatchLaterLastReorderTime().toEpochSecond(),
|
|
||||||
_watchlistOrderStore.values.toList()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to broadcast watch later", e)
|
Logger.w(TAG, "Failed to broadcast watch later", e)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.states
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.LoginActivity
|
import com.futo.platformplayer.activities.LoginActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
@ -101,6 +102,8 @@ class StatePlugins {
|
||||||
if (availableClient !is JSClient) {
|
if (availableClient !is JSClient) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && !StatePlatform.instance.isClientEnabled(availableClient.id))
|
||||||
|
continue;
|
||||||
|
|
||||||
val newConfig = checkForUpdates(availableClient.config);
|
val newConfig = checkForUpdates(availableClient.config);
|
||||||
if (newConfig != null) {
|
if (newConfig != null) {
|
||||||
|
|
|
@ -21,9 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.awaitFirstDeferred
|
import com.futo.platformplayer.awaitFirstDeferred
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
|
@ -33,6 +31,7 @@ import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ClaimType
|
import com.futo.polycentric.core.ClaimType
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
import com.futo.polycentric.core.SignedEvent
|
import com.futo.polycentric.core.SignedEvent
|
||||||
|
@ -234,34 +233,7 @@ class StatePolycentric {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return Pair(false, listOf(url));
|
return Pair(false, listOf(url));
|
||||||
}
|
}
|
||||||
var polycentricProfile: PolycentricProfile? = null;
|
return Pair(didUpdate, listOf(url));
|
||||||
try {
|
|
||||||
val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly)
|
|
||||||
polycentricProfile = polycentricCached?.profile;
|
|
||||||
if (polycentricCached == null && channelId != null) {
|
|
||||||
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
|
|
||||||
if(!cacheOnly) {
|
|
||||||
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile;
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.i("StateSubscriptions", "Get polycentric profile cached");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
|
|
||||||
//TODO: Some way to communicate polycentric failing without blocking here
|
|
||||||
}
|
|
||||||
if(polycentricProfile != null) {
|
|
||||||
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
|
|
||||||
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
|
|
||||||
if(urls.any { it.equals(url, true) })
|
|
||||||
return Pair(didUpdate, urls);
|
|
||||||
else
|
|
||||||
return Pair(didUpdate, listOf(url) + urls);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return Pair(didUpdate, listOf(url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
||||||
|
@ -325,7 +297,7 @@ class StatePolycentric {
|
||||||
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = systemState.username,
|
name = systemState.username,
|
||||||
url = author,
|
url = author,
|
||||||
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
@ -349,7 +321,7 @@ class StatePolycentric {
|
||||||
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
||||||
ensureEnabled()
|
ensureEnabled()
|
||||||
|
|
||||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||||
null,
|
null,
|
||||||
listOf(
|
listOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
|
@ -382,7 +354,7 @@ class StatePolycentric {
|
||||||
}
|
}
|
||||||
|
|
||||||
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
||||||
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
val events = ApiMethods.getEvents(ApiMethods.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
||||||
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
||||||
.setProcess(pointer.process)
|
.setProcess(pointer.process)
|
||||||
.addRanges(Protocol.Range.newBuilder()
|
.addRanges(Protocol.Range.newBuilder()
|
||||||
|
@ -400,11 +372,11 @@ class StatePolycentric {
|
||||||
}
|
}
|
||||||
|
|
||||||
val post = Protocol.Post.parseFrom(ev.content);
|
val post = Protocol.Post.parseFrom(ev.content);
|
||||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
||||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
|
|
||||||
val profileEvents = ApiMethods.getQueryLatest(
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
PolycentricCache.SERVER,
|
ApiMethods.SERVER,
|
||||||
ev.system.toProto(),
|
ev.system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
|
@ -433,7 +405,7 @@ class StatePolycentric {
|
||||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
url = systemLinkUrl,
|
url = systemLinkUrl,
|
||||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
@ -445,12 +417,12 @@ class StatePolycentric {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
suspend fun getCommentPager(contextUrl: String, reference: Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.addAllCountLwwElementReferences(arrayListOf(
|
.addAllCountLwwElementReferences(arrayListOf(
|
||||||
|
@ -486,7 +458,7 @@ class StatePolycentric {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun nextPageAsync() {
|
override suspend fun nextPageAsync() {
|
||||||
val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor,
|
val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor,
|
||||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.addAllCountLwwElementReferences(arrayListOf(
|
.addAllCountLwwElementReferences(arrayListOf(
|
||||||
|
@ -534,7 +506,7 @@ class StatePolycentric {
|
||||||
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
||||||
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
||||||
val profileEvents = ApiMethods.getQueryLatest(
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
PolycentricCache.SERVER,
|
ApiMethods.SERVER,
|
||||||
ev.system.toProto(),
|
ev.system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
|
@ -558,7 +530,7 @@ class StatePolycentric {
|
||||||
|
|
||||||
val unixMilliseconds = ev.unixMilliseconds
|
val unixMilliseconds = ev.unixMilliseconds
|
||||||
//TODO: Don't use single hardcoded sderver here
|
//TODO: Don't use single hardcoded sderver here
|
||||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
||||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
return@async PolycentricPlatformComment(
|
return@async PolycentricPlatformComment(
|
||||||
contextUrl = contextUrl,
|
contextUrl = contextUrl,
|
||||||
|
@ -566,7 +538,7 @@ class StatePolycentric {
|
||||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
url = systemLinkUrl,
|
url = systemLinkUrl,
|
||||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
|
|
@ -1,54 +1,17 @@
|
||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import com.futo.platformplayer.constructs.Event2
|
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.getNowDiffDays
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
|
||||||
import com.futo.platformplayer.states.StateHistory.Companion
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
|
||||||
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
|
||||||
import java.util.concurrent.ForkJoinPool
|
|
||||||
import java.util.concurrent.ForkJoinTask
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlin.streams.asSequence
|
|
||||||
import kotlin.streams.toList
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Used to maintain subscription groups
|
* Used to maintain subscription groups
|
||||||
|
|
|
@ -15,7 +15,6 @@ import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
|
@ -335,12 +334,6 @@ class StateSubscriptions {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: This causes issues, because what if the profile is not cached yet when the susbcribe button is loaded for example?
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(urls.first(), true)?.profile;
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
return cachedProfile.ownedClaims.any { c -> _subscriptions.hasItem { s -> c.claim.resolveChannelUrl() == s.channel.url } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class StateSync {
|
class StateSync {
|
||||||
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
|
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
|
||||||
|
private val _nameStorage = FragmentedStorage.get<StringStringMapStorage>("sync_remembered_name_storage")
|
||||||
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
|
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
|
||||||
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
||||||
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
||||||
|
@ -305,12 +306,22 @@ class StateSync {
|
||||||
synchronized(_sessions) {
|
synchronized(_sessions) {
|
||||||
session = _sessions[s.remotePublicKey]
|
session = _sessions[s.remotePublicKey]
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
|
val remoteDeviceName = synchronized(_nameStorage) {
|
||||||
|
_nameStorage.get(remotePublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
||||||
if (!isNewSession) {
|
if (!isNewSession) {
|
||||||
return@SyncSession
|
return@SyncSession
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "${s.remotePublicKey} authorized")
|
it.remoteDeviceName?.let { remoteDeviceName ->
|
||||||
|
synchronized(_nameStorage) {
|
||||||
|
_nameStorage.setAndSave(remotePublicKey, remoteDeviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "${s.remotePublicKey} authorized (name: ${it.displayName})")
|
||||||
synchronized(_lastAddressStorage) {
|
synchronized(_lastAddressStorage) {
|
||||||
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
||||||
}
|
}
|
||||||
|
@ -341,7 +352,7 @@ class StateSync {
|
||||||
|
|
||||||
deviceRemoved.emit(it.remotePublicKey)
|
deviceRemoved.emit(it.remotePublicKey)
|
||||||
|
|
||||||
})
|
}, remoteDeviceName)
|
||||||
_sessions[remotePublicKey] = session!!
|
_sessions[remotePublicKey] = session!!
|
||||||
}
|
}
|
||||||
session!!.addSocketSession(s)
|
session!!.addSocketSession(s)
|
||||||
|
@ -469,6 +480,12 @@ class StateSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCachedName(publicKey: String): String? {
|
||||||
|
return synchronized(_nameStorage) {
|
||||||
|
_nameStorage.get(publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun delete(publicKey: String) {
|
suspend fun delete(publicKey: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
package com.futo.platformplayer.stores
|
|
||||||
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
|
||||||
class CachedPolycentricProfileStorage : FragmentedStorageFileJson() {
|
|
||||||
var map: HashMap<String, PolycentricCache.CachedPolycentricProfile> = hashMapOf();
|
|
||||||
|
|
||||||
override fun encode(): String {
|
|
||||||
val encoded = Json.encodeToString(this);
|
|
||||||
return encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(key: String) : PolycentricCache.CachedPolycentricProfile? {
|
|
||||||
return map[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAndSave(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
|
|
||||||
map[key] = value;
|
|
||||||
save();
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAndSaveBlocking(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
|
|
||||||
map[key] = value;
|
|
||||||
saveBlocking();
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,6 +33,9 @@ class ManagedStore<T>{
|
||||||
|
|
||||||
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
|
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
|
||||||
|
|
||||||
|
private var _onModificationCreate: ((T) -> Unit)? = null;
|
||||||
|
private var _onModificationDelete: ((T) -> Unit)? = null;
|
||||||
|
|
||||||
val name: String;
|
val name: String;
|
||||||
|
|
||||||
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
|
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
|
||||||
|
@ -62,6 +65,12 @@ class ManagedStore<T>{
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore<T> {
|
||||||
|
_onModificationCreate = created;
|
||||||
|
_onModificationDelete = deleted;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
fun load(): ManagedStore<T> {
|
fun load(): ManagedStore<T> {
|
||||||
synchronized(_files) {
|
synchronized(_files) {
|
||||||
_files.clear();
|
_files.clear();
|
||||||
|
@ -265,6 +274,7 @@ class ManagedStore<T>{
|
||||||
file = saveNew(obj);
|
file = saveNew(obj);
|
||||||
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
|
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
|
||||||
saveReconstruction(file, obj);
|
saveReconstruction(file, obj);
|
||||||
|
_onModificationCreate?.invoke(obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,6 +310,7 @@ class ManagedStore<T>{
|
||||||
_files.remove(item);
|
_files.remove(item);
|
||||||
Logger.v(TAG, "Deleting file ${logName(file.id)}");
|
Logger.v(TAG, "Deleting file ${logName(file.id)}");
|
||||||
file.delete();
|
file.delete();
|
||||||
|
_onModificationDelete?.invoke(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,10 @@ import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.HistoryVideo
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
|
||||||
import com.futo.platformplayer.smartMerge
|
import com.futo.platformplayer.smartMerge
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
@ -30,6 +28,7 @@ import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
@ -53,6 +52,9 @@ class SyncSession : IAuthorizable {
|
||||||
private val _id = UUID.randomUUID()
|
private val _id = UUID.randomUUID()
|
||||||
private var _remoteId: UUID? = null
|
private var _remoteId: UUID? = null
|
||||||
private var _lastAuthorizedRemoteId: UUID? = null
|
private var _lastAuthorizedRemoteId: UUID? = null
|
||||||
|
var remoteDeviceName: String? = null
|
||||||
|
private set
|
||||||
|
val displayName: String get() = remoteDeviceName ?: remotePublicKey
|
||||||
|
|
||||||
var connected: Boolean = false
|
var connected: Boolean = false
|
||||||
private set(v) {
|
private set(v) {
|
||||||
|
@ -62,7 +64,7 @@ class SyncSession : IAuthorizable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
|
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit, remoteDeviceName: String?) {
|
||||||
this.remotePublicKey = remotePublicKey
|
this.remotePublicKey = remotePublicKey
|
||||||
_onAuthorized = onAuthorized
|
_onAuthorized = onAuthorized
|
||||||
_onUnauthorized = onUnauthorized
|
_onUnauthorized = onUnauthorized
|
||||||
|
@ -85,7 +87,20 @@ class SyncSession : IAuthorizable {
|
||||||
|
|
||||||
fun authorize(socketSession: SyncSocketSession) {
|
fun authorize(socketSession: SyncSocketSession) {
|
||||||
Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
|
Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
|
||||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
|
||||||
|
if (socketSession.remoteVersion >= 3) {
|
||||||
|
val idStringBytes = _id.toString().toByteArray()
|
||||||
|
val nameBytes = "${android.os.Build.MANUFACTURER}-${android.os.Build.MODEL}".toByteArray()
|
||||||
|
val buffer = ByteArray(1 + idStringBytes.size + 1 + nameBytes.size)
|
||||||
|
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||||
|
put(idStringBytes.size.toByte())
|
||||||
|
put(idStringBytes)
|
||||||
|
put(nameBytes.size.toByte())
|
||||||
|
put(nameBytes)
|
||||||
|
}.apply { flip() })
|
||||||
|
} else {
|
||||||
|
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||||
|
}
|
||||||
_authorized = true
|
_authorized = true
|
||||||
checkAuthorized()
|
checkAuthorized()
|
||||||
}
|
}
|
||||||
|
@ -138,15 +153,37 @@ class SyncSession : IAuthorizable {
|
||||||
|
|
||||||
when (opcode) {
|
when (opcode) {
|
||||||
Opcode.NOTIFY_AUTHORIZED.value -> {
|
Opcode.NOTIFY_AUTHORIZED.value -> {
|
||||||
val str = data.toUtf8String()
|
if (socketSession.remoteVersion >= 3) {
|
||||||
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
val idByteCount = data.get().toInt()
|
||||||
|
if (idByteCount > 64)
|
||||||
|
throw Exception("Id should always be smaller than 64 bytes")
|
||||||
|
|
||||||
|
val idBytes = ByteArray(idByteCount)
|
||||||
|
data.get(idBytes)
|
||||||
|
|
||||||
|
val nameByteCount = data.get().toInt()
|
||||||
|
if (nameByteCount > 64)
|
||||||
|
throw Exception("Name should always be smaller than 64 bytes")
|
||||||
|
|
||||||
|
val nameBytes = ByteArray(nameByteCount)
|
||||||
|
data.get(nameBytes)
|
||||||
|
|
||||||
|
_remoteId = UUID.fromString(idBytes.toString(Charsets.UTF_8))
|
||||||
|
remoteDeviceName = nameBytes.toString(Charsets.UTF_8)
|
||||||
|
} else {
|
||||||
|
val str = data.toUtf8String()
|
||||||
|
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||||
|
remoteDeviceName = null
|
||||||
|
}
|
||||||
|
|
||||||
_remoteAuthorized = true
|
_remoteAuthorized = true
|
||||||
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId")
|
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId (device name: '${remoteDeviceName ?: "not set"}')")
|
||||||
checkAuthorized()
|
checkAuthorized()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
||||||
_remoteId = null
|
_remoteId = null
|
||||||
|
remoteDeviceName = null
|
||||||
_lastAuthorizedRemoteId = null
|
_lastAuthorizedRemoteId = null
|
||||||
_remoteAuthorized = false
|
_remoteAuthorized = false
|
||||||
_onUnauthorized(this)
|
_onUnauthorized(this)
|
||||||
|
@ -195,6 +232,8 @@ class SyncSession : IAuthorizable {
|
||||||
sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
|
sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
|
||||||
sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
|
sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
|
||||||
|
|
||||||
|
sendData(GJSyncOpcodes.syncWatchLater, Json.encodeToString(StatePlaylists.instance.getWatchLaterSyncPacket(false)));
|
||||||
|
|
||||||
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
|
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
|
||||||
if(recentHistory.size > 0)
|
if(recentHistory.size > 0)
|
||||||
sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);
|
sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);
|
||||||
|
|
|
@ -46,6 +46,8 @@ class SyncSocketSession {
|
||||||
val localPublicKey: String get() = _localPublicKey
|
val localPublicKey: String get() = _localPublicKey
|
||||||
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
|
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
|
||||||
var authorizable: IAuthorizable? = null
|
var authorizable: IAuthorizable? = null
|
||||||
|
var remoteVersion: Int = -1
|
||||||
|
private set
|
||||||
|
|
||||||
val remoteAddress: String
|
val remoteAddress: String
|
||||||
|
|
||||||
|
@ -162,11 +164,12 @@ class SyncSocketSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performVersionCheck() {
|
private fun performVersionCheck() {
|
||||||
val CURRENT_VERSION = 2
|
val CURRENT_VERSION = 3
|
||||||
|
val MINIMUM_VERSION = 2
|
||||||
_outputStream.writeInt(CURRENT_VERSION)
|
_outputStream.writeInt(CURRENT_VERSION)
|
||||||
val version = _inputStream.readInt()
|
remoteVersion = _inputStream.readInt()
|
||||||
Logger.i(TAG, "performVersionCheck (version = $version)")
|
Logger.i(TAG, "performVersionCheck (version = $remoteVersion)")
|
||||||
if (version != CURRENT_VERSION)
|
if (remoteVersion < MINIMUM_VERSION)
|
||||||
throw Exception("Invalid version")
|
throw Exception("Invalid version")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,11 @@ import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@ -125,8 +125,7 @@ class MonetizationView : LinearLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?) {
|
fun setPolycentricProfile(profile: PolycentricProfile?) {
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
if (profile.systemState.store.isNotEmpty()) {
|
if (profile.systemState.store.isNotEmpty()) {
|
||||||
_buttonStore.visibility = View.VISIBLE;
|
_buttonStore.visibility = View.VISIBLE;
|
||||||
|
|
|
@ -14,10 +14,10 @@ import androidx.core.view.isVisible
|
||||||
import androidx.core.view.size
|
import androidx.core.view.size
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
|
||||||
|
|
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.ChannelMonetizationFragment
|
||||||
import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment
|
import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
|
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
@ -29,6 +27,7 @@ import com.futo.platformplayer.views.LoaderView
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.pills.PillButton
|
import com.futo.platformplayer.views.pills.PillButton
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -81,24 +80,18 @@ class CommentViewHolder : ViewHolder {
|
||||||
throw Exception("Not implemented for non polycentric comments")
|
throw Exception("Not implemented for non polycentric comments")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.hasLiked) {
|
val newOpinion: Opinion = if (args.hasLiked) {
|
||||||
args.processHandle.opinion(c.reference, Opinion.like);
|
Opinion.like
|
||||||
} else if (args.hasDisliked) {
|
} else if (args.hasDisliked) {
|
||||||
args.processHandle.opinion(c.reference, Opinion.dislike);
|
Opinion.dislike
|
||||||
} else {
|
} else {
|
||||||
args.processHandle.opinion(c.reference, Opinion.neutral);
|
Opinion.neutral
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
ApiMethods.setOpinion(args.processHandle, c.reference, newOpinion)
|
||||||
Logger.i(TAG, "Started backfill");
|
|
||||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
|
||||||
Logger.i(TAG, "Finished backfill");
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to backfill servers.", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||||
|
|
|
@ -16,7 +16,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
@ -26,6 +25,7 @@ import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.pills.PillButton
|
import com.futo.platformplayer.views.pills.PillButton
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.IdentityHashMap
|
import java.util.IdentityHashMap
|
||||||
|
|
|
@ -16,7 +16,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
@ -29,21 +28,12 @@ open class PlaylistView : LinearLayout {
|
||||||
protected val _imageThumbnail: ImageView
|
protected val _imageThumbnail: ImageView
|
||||||
protected val _imageChannel: ImageView?
|
protected val _imageChannel: ImageView?
|
||||||
protected val _creatorThumbnail: CreatorThumbnail?
|
protected val _creatorThumbnail: CreatorThumbnail?
|
||||||
protected val _imageNeopassChannel: ImageView?;
|
|
||||||
protected val _platformIndicator: PlatformIndicator;
|
protected val _platformIndicator: PlatformIndicator;
|
||||||
protected val _textPlaylistName: TextView
|
protected val _textPlaylistName: TextView
|
||||||
protected val _textVideoCount: TextView
|
protected val _textVideoCount: TextView
|
||||||
protected val _textVideoCountLabel: TextView;
|
protected val _textVideoCountLabel: TextView;
|
||||||
protected val _textPlaylistItems: TextView
|
protected val _textPlaylistItems: TextView
|
||||||
protected val _textChannelName: TextView
|
protected val _textChannelName: TextView
|
||||||
protected var _neopassAnimator: ObjectAnimator? = null;
|
|
||||||
|
|
||||||
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
|
|
||||||
.success { it -> updateClaimsLayout(it, animate = true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
val onPlaylistClicked = Event1<IPlatformPlaylist>();
|
val onPlaylistClicked = Event1<IPlatformPlaylist>();
|
||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
|
@ -66,7 +56,6 @@ open class PlaylistView : LinearLayout {
|
||||||
_textVideoCountLabel = findViewById(R.id.text_video_count_label);
|
_textVideoCountLabel = findViewById(R.id.text_video_count_label);
|
||||||
_textChannelName = findViewById(R.id.text_channel_name);
|
_textChannelName = findViewById(R.id.text_channel_name);
|
||||||
_textPlaylistItems = findViewById(R.id.text_playlist_items);
|
_textPlaylistItems = findViewById(R.id.text_playlist_items);
|
||||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
|
||||||
|
|
||||||
setOnClickListener { onOpenClicked() };
|
setOnClickListener { onOpenClicked() };
|
||||||
_imageChannel?.setOnClickListener { currentPlaylist?.let { onChannelClicked.emit(it.author) } };
|
_imageChannel?.setOnClickListener { currentPlaylist?.let { onChannelClicked.emit(it.author) } };
|
||||||
|
@ -88,20 +77,6 @@ open class PlaylistView : LinearLayout {
|
||||||
|
|
||||||
|
|
||||||
open fun bind(content: IPlatformContent) {
|
open fun bind(content: IPlatformContent) {
|
||||||
_taskLoadValidClaims.cancel();
|
|
||||||
|
|
||||||
if (content.author.id.claimType > 0) {
|
|
||||||
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
|
|
||||||
if (cachedClaims != null) {
|
|
||||||
updateClaimsLayout(cachedClaims, animate = false);
|
|
||||||
} else {
|
|
||||||
updateClaimsLayout(null, animate = false);
|
|
||||||
_taskLoadValidClaims.run(content.author.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateClaimsLayout(null, animate = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
isClickable = true;
|
isClickable = true;
|
||||||
|
|
||||||
_imageChannel?.let {
|
_imageChannel?.let {
|
||||||
|
@ -155,25 +130,6 @@ open class PlaylistView : LinearLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
|
|
||||||
_neopassAnimator?.cancel();
|
|
||||||
_neopassAnimator = null;
|
|
||||||
|
|
||||||
val firstClaim = claims?.ownedClaims?.firstOrNull();
|
|
||||||
val harborAvailable = firstClaim != null
|
|
||||||
if (harborAvailable) {
|
|
||||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
|
||||||
if (animate) {
|
|
||||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
|
||||||
_neopassAnimator?.start()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_imageNeopassChannel?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "VideoPreviewViewHolder"
|
private val TAG = "VideoPreviewViewHolder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||||
private lateinit var _sortedDataset: List<Subscription>;
|
private lateinit var _sortedDataset: List<Subscription>;
|
||||||
private val _inflater: LayoutInflater;
|
private val _inflater: LayoutInflater;
|
||||||
private val _confirmationMessage: String;
|
private val _confirmationMessage: String;
|
||||||
|
private val _onDatasetChanged: ((List<Subscription>)->Unit)?;
|
||||||
|
|
||||||
var onClick = Event1<Subscription>();
|
var onClick = Event1<Subscription>();
|
||||||
var onSettings = Event1<Subscription>();
|
var onSettings = Event1<Subscription>();
|
||||||
|
@ -30,9 +31,10 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||||
updateDataset();
|
updateDataset();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(inflater: LayoutInflater, confirmationMessage: String) : super() {
|
constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
|
||||||
_inflater = inflater;
|
_inflater = inflater;
|
||||||
_confirmationMessage = confirmationMessage;
|
_confirmationMessage = confirmationMessage;
|
||||||
|
_onDatasetChanged = onDatasetChanged;
|
||||||
|
|
||||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() }
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() }
|
||||||
|
@ -78,6 +80,8 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||||
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
|
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
_onDatasetChanged?.invoke(_sortedDataset);
|
||||||
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanTimeIndicator
|
import com.futo.platformplayer.toHumanTimeIndicator
|
||||||
|
@ -32,14 +31,6 @@ class SubscriptionViewHolder : ViewHolder {
|
||||||
private val _platformIndicator : PlatformIndicator;
|
private val _platformIndicator : PlatformIndicator;
|
||||||
private val _textMeta: TextView;
|
private val _textMeta: TextView;
|
||||||
|
|
||||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
|
||||||
StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
|
||||||
.success { it -> onProfileLoaded(null, it, true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
var subscription: Subscription? = null
|
var subscription: Subscription? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
|
@ -74,45 +65,12 @@ class SubscriptionViewHolder : ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(sub: Subscription) {
|
fun bind(sub: Subscription) {
|
||||||
_taskLoadProfile.cancel();
|
|
||||||
|
|
||||||
this.subscription = sub;
|
this.subscription = sub;
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
||||||
_textName.text = sub.channel.name;
|
_textName.text = sub.channel.name;
|
||||||
bindViewMetrics(sub);
|
bindViewMetrics(sub);
|
||||||
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
onProfileLoaded(sub, cachedProfile, false);
|
|
||||||
if (cachedProfile.expired) {
|
|
||||||
_taskLoadProfile.run(sub.channel.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadProfile.run(sub.channel.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
|
||||||
val dp_46 = 46.dp(itemView.context.resources);
|
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
|
||||||
} else {
|
|
||||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
_textName.text = profile.systemState.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(sub != null)
|
|
||||||
bindViewMetrics(sub)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bindViewMetrics(sub: Subscription?) {
|
fun bindViewMetrics(sub: Subscription?) {
|
||||||
|
|
|
@ -30,7 +30,6 @@ import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanNowDiffString
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
@ -44,7 +43,6 @@ class PreviewPostView : LinearLayout {
|
||||||
|
|
||||||
private val _imageAuthorThumbnail: ImageView;
|
private val _imageAuthorThumbnail: ImageView;
|
||||||
private val _textAuthorName: TextView;
|
private val _textAuthorName: TextView;
|
||||||
private val _imageNeopassChannel: ImageView;
|
|
||||||
private val _textMetadata: TextView;
|
private val _textMetadata: TextView;
|
||||||
private val _textTitle: TextView;
|
private val _textTitle: TextView;
|
||||||
private val _textDescription: TextView;
|
private val _textDescription: TextView;
|
||||||
|
@ -64,15 +62,6 @@ class PreviewPostView : LinearLayout {
|
||||||
private val _layoutComments: LinearLayout?;
|
private val _layoutComments: LinearLayout?;
|
||||||
private val _textComments: TextView?;
|
private val _textComments: TextView?;
|
||||||
|
|
||||||
private var _neopassAnimator: ObjectAnimator? = null;
|
|
||||||
|
|
||||||
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
|
|
||||||
.success { it -> updateClaimsLayout(it, animate = true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
val content: IPlatformContent? get() = _content;
|
val content: IPlatformContent? get() = _content;
|
||||||
|
|
||||||
val onContentClicked = Event1<IPlatformContent>();
|
val onContentClicked = Event1<IPlatformContent>();
|
||||||
|
@ -83,7 +72,6 @@ class PreviewPostView : LinearLayout {
|
||||||
|
|
||||||
_imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail);
|
_imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail);
|
||||||
_textAuthorName = findViewById(R.id.text_author_name);
|
_textAuthorName = findViewById(R.id.text_author_name);
|
||||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
|
||||||
_textMetadata = findViewById(R.id.text_metadata);
|
_textMetadata = findViewById(R.id.text_metadata);
|
||||||
_textTitle = findViewById(R.id.text_title);
|
_textTitle = findViewById(R.id.text_title);
|
||||||
_textDescription = findViewById(R.id.text_description);
|
_textDescription = findViewById(R.id.text_description);
|
||||||
|
@ -130,21 +118,8 @@ class PreviewPostView : LinearLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(content: IPlatformContent) {
|
fun bind(content: IPlatformContent) {
|
||||||
_taskLoadValidClaims.cancel();
|
|
||||||
_content = content;
|
_content = content;
|
||||||
|
|
||||||
if (content.author.id.claimType > 0) {
|
|
||||||
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
|
|
||||||
if (cachedClaims != null) {
|
|
||||||
updateClaimsLayout(cachedClaims, animate = false);
|
|
||||||
} else {
|
|
||||||
updateClaimsLayout(null, animate = false);
|
|
||||||
_taskLoadValidClaims.run(content.author.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateClaimsLayout(null, animate = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_textAuthorName.text = content.author.name;
|
_textAuthorName.text = content.author.name;
|
||||||
_textMetadata.text = content.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "";
|
_textMetadata.text = content.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "";
|
||||||
|
|
||||||
|
@ -292,25 +267,6 @@ class PreviewPostView : LinearLayout {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
|
|
||||||
_neopassAnimator?.cancel();
|
|
||||||
_neopassAnimator = null;
|
|
||||||
|
|
||||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
|
||||||
if (harborAvailable) {
|
|
||||||
_imageNeopassChannel.visibility = View.VISIBLE
|
|
||||||
if (animate) {
|
|
||||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
|
||||||
_neopassAnimator?.start()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_imageNeopassChannel.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Necessary if we decide to use creator thumbnail with neopass indicator instead
|
|
||||||
//_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "PreviewPostView";
|
val TAG = "PreviewPostView";
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import com.futo.platformplayer.getNowDiffSeconds
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails
|
import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
@ -47,7 +46,6 @@ open class PreviewVideoView : LinearLayout {
|
||||||
protected val _imageVideo: ImageView
|
protected val _imageVideo: ImageView
|
||||||
protected val _imageChannel: ImageView?
|
protected val _imageChannel: ImageView?
|
||||||
protected val _creatorThumbnail: CreatorThumbnail?
|
protected val _creatorThumbnail: CreatorThumbnail?
|
||||||
protected val _imageNeopassChannel: ImageView?;
|
|
||||||
protected val _platformIndicator: PlatformIndicator;
|
protected val _platformIndicator: PlatformIndicator;
|
||||||
protected val _textVideoName: TextView
|
protected val _textVideoName: TextView
|
||||||
protected val _textChannelName: TextView
|
protected val _textChannelName: TextView
|
||||||
|
@ -57,7 +55,6 @@ open class PreviewVideoView : LinearLayout {
|
||||||
protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null;
|
protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null;
|
||||||
protected val _containerLive: LinearLayout;
|
protected val _containerLive: LinearLayout;
|
||||||
protected val _playerContainer: FrameLayout;
|
protected val _playerContainer: FrameLayout;
|
||||||
protected var _neopassAnimator: ObjectAnimator? = null;
|
|
||||||
protected val _layoutDownloaded: FrameLayout;
|
protected val _layoutDownloaded: FrameLayout;
|
||||||
|
|
||||||
protected val _button_add_to_queue : View;
|
protected val _button_add_to_queue : View;
|
||||||
|
@ -65,15 +62,6 @@ open class PreviewVideoView : LinearLayout {
|
||||||
protected val _button_add_to : View;
|
protected val _button_add_to : View;
|
||||||
|
|
||||||
protected val _exoPlayer: PlayerManager?;
|
protected val _exoPlayer: PlayerManager?;
|
||||||
|
|
||||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
|
||||||
StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
|
||||||
.success { it -> onProfileLoaded(it, true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
private val _timeBar: ProgressBar?;
|
private val _timeBar: ProgressBar?;
|
||||||
|
|
||||||
val onVideoClicked = Event2<IPlatformVideo, Long>();
|
val onVideoClicked = Event2<IPlatformVideo, Long>();
|
||||||
|
@ -108,7 +96,6 @@ open class PreviewVideoView : LinearLayout {
|
||||||
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
||||||
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
|
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
|
||||||
_button_add_to = findViewById(R.id.button_add_to);
|
_button_add_to = findViewById(R.id.button_add_to);
|
||||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
|
||||||
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
||||||
_timeBar = findViewById(R.id.time_bar)
|
_timeBar = findViewById(R.id.time_bar)
|
||||||
|
|
||||||
|
@ -132,7 +119,7 @@ open class PreviewVideoView : LinearLayout {
|
||||||
|
|
||||||
fun hideAddTo() {
|
fun hideAddTo() {
|
||||||
_button_add_to.visibility = View.GONE
|
_button_add_to.visibility = View.GONE
|
||||||
_button_add_to_queue.visibility = View.GONE
|
//_button_add_to_queue.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun inflate(feedStyle: FeedStyle) {
|
protected open fun inflate(feedStyle: FeedStyle) {
|
||||||
|
@ -160,15 +147,12 @@ open class PreviewVideoView : LinearLayout {
|
||||||
|
|
||||||
|
|
||||||
open fun bind(content: IPlatformContent) {
|
open fun bind(content: IPlatformContent) {
|
||||||
_taskLoadProfile.cancel();
|
|
||||||
|
|
||||||
isClickable = true;
|
isClickable = true;
|
||||||
|
|
||||||
val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0;
|
val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0;
|
||||||
|
|
||||||
stopPreview();
|
stopPreview();
|
||||||
|
|
||||||
_imageNeopassChannel?.visibility = View.GONE;
|
|
||||||
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
||||||
|
|
||||||
val thumbnail = content.author.thumbnail
|
val thumbnail = content.author.thumbnail
|
||||||
|
@ -186,16 +170,6 @@ open class PreviewVideoView : LinearLayout {
|
||||||
|
|
||||||
_textChannelName.text = content.author.name
|
_textChannelName.text = content.author.name
|
||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
onProfileLoaded(cachedProfile, false);
|
|
||||||
if (cachedProfile.expired) {
|
|
||||||
_taskLoadProfile.run(content.author.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadProfile.run(content.author.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageChannel?.clipToOutline = true;
|
_imageChannel?.clipToOutline = true;
|
||||||
|
|
||||||
_textVideoName.text = content.name;
|
_textVideoName.text = content.name;
|
||||||
|
@ -335,52 +309,6 @@ open class PreviewVideoView : LinearLayout {
|
||||||
_playerVideoThumbnail?.setMuteChangedListener(callback);
|
_playerVideoThumbnail?.setMuteChangedListener(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
|
||||||
_neopassAnimator?.cancel();
|
|
||||||
_neopassAnimator = null;
|
|
||||||
|
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
if (_creatorThumbnail != null) {
|
|
||||||
val dp_32 = 32.dp(context.resources);
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_32 * dp_32)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
|
||||||
} else {
|
|
||||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
|
||||||
}
|
|
||||||
} else if (_imageChannel != null) {
|
|
||||||
val dp_28 = 28.dp(context.resources);
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_28 * dp_28)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
_imageChannel.let {
|
|
||||||
Glide.with(_imageChannel)
|
|
||||||
.load(avatar)
|
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
|
||||||
.into(_imageChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
|
||||||
if (animate) {
|
|
||||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
|
||||||
_neopassAnimator?.start()
|
|
||||||
} else {
|
|
||||||
_imageNeopassChannel?.alpha = 1.0f;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_imageNeopassChannel?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
_textChannelName.text = profile.systemState.username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "VideoPreviewViewHolder"
|
private val TAG = "VideoPreviewViewHolder"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
@ -27,14 +26,6 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
||||||
|
|
||||||
val onClick = Event1<IPlatformChannel>();
|
val onClick = Event1<IPlatformChannel>();
|
||||||
|
|
||||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
|
||||||
StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
|
||||||
.success { onProfileLoaded(it, true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
||||||
_name = _view.findViewById(R.id.text_channel_name);
|
_name = _view.findViewById(R.id.text_channel_name);
|
||||||
|
@ -45,40 +36,10 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(value: IPlatformChannel) {
|
override fun bind(value: IPlatformChannel) {
|
||||||
_taskLoadProfile.cancel();
|
|
||||||
|
|
||||||
_channel = value;
|
_channel = value;
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(value.thumbnail, false);
|
_creatorThumbnail.setThumbnail(value.thumbnail, false);
|
||||||
_name.text = value.name;
|
_name.text = value.name;
|
||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
onProfileLoaded(cachedProfile, false);
|
|
||||||
if (cachedProfile.expired) {
|
|
||||||
_taskLoadProfile.run(value.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadProfile.run(value.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
|
||||||
val dp_55 = 55.dp(itemView.context.resources)
|
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
|
||||||
} else {
|
|
||||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
_name.text = profile.systemState.username;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -94,14 +55,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||||
|
|
||||||
val onClick = Event1<Selectable>();
|
val onClick = Event1<Selectable>();
|
||||||
|
|
||||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
|
||||||
StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
|
||||||
.success { onProfileLoaded(it, true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
||||||
_name = _view.findViewById(R.id.text_channel_name);
|
_name = _view.findViewById(R.id.text_channel_name);
|
||||||
|
@ -112,8 +65,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(value: Selectable) {
|
override fun bind(value: Selectable) {
|
||||||
_taskLoadProfile.cancel();
|
|
||||||
|
|
||||||
_channel = value;
|
_channel = value;
|
||||||
|
|
||||||
if(value.active)
|
if(value.active)
|
||||||
|
@ -123,34 +74,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
|
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
|
||||||
_name.text = value.channel.name;
|
_name.text = value.channel.name;
|
||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
onProfileLoaded(cachedProfile, false);
|
|
||||||
if (cachedProfile.expired) {
|
|
||||||
_taskLoadProfile.run(value.channel.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadProfile.run(value.channel.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
|
||||||
val dp_55 = 55.dp(itemView.context.resources)
|
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
|
||||||
} else {
|
|
||||||
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
|
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
_name.text = profile.systemState.username;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanNumber
|
import com.futo.platformplayer.toHumanNumber
|
||||||
|
@ -34,14 +33,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||||
|
|
||||||
val onClick = Event1<PlatformAuthorLink>();
|
val onClick = Event1<PlatformAuthorLink>();
|
||||||
|
|
||||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
|
||||||
StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
|
||||||
.success { it -> onProfileLoaded(it, true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_textName = _view.findViewById(R.id.text_channel_name);
|
_textName = _view.findViewById(R.id.text_channel_name);
|
||||||
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
||||||
|
@ -61,21 +52,9 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(value: PlatformAuthorLink) {
|
override fun bind(value: PlatformAuthorLink) {
|
||||||
_taskLoadProfile.cancel();
|
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(value.thumbnail, false);
|
_creatorThumbnail.setThumbnail(value.thumbnail, false);
|
||||||
_textName.text = value.name;
|
_textName.text = value.name;
|
||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
onProfileLoaded(cachedProfile, false);
|
|
||||||
if (cachedProfile.expired) {
|
|
||||||
_taskLoadProfile.run(value.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadProfile.run(value.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(value.subscribers == null || (value.subscribers ?: 0) <= 0L)
|
if(value.subscribers == null || (value.subscribers ?: 0) <= 0L)
|
||||||
_textMetadata.visibility = View.GONE;
|
_textMetadata.visibility = View.GONE;
|
||||||
else {
|
else {
|
||||||
|
@ -87,25 +66,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||||
_authorLink = value;
|
_authorLink = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
|
||||||
val dp_61 = 61.dp(itemView.context.resources);
|
|
||||||
|
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_61 * dp_61)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
|
||||||
} else {
|
|
||||||
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
|
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
_textName.text = profile.systemState.username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CreatorViewHolder";
|
private const val TAG = "CreatorViewHolder";
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
@ -27,14 +26,6 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||||
private var _subscription: Subscription? = null;
|
private var _subscription: Subscription? = null;
|
||||||
private var _channel: SerializedChannel? = null;
|
private var _channel: SerializedChannel? = null;
|
||||||
|
|
||||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
|
||||||
StateApp.instance.scopeGetter,
|
|
||||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
|
||||||
.success { onProfileLoaded(it, true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
|
||||||
};
|
|
||||||
|
|
||||||
val onClick = Event1<Subscription>();
|
val onClick = Event1<Subscription>();
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -47,44 +38,14 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(value: Subscription) {
|
override fun bind(value: Subscription) {
|
||||||
_taskLoadProfile.cancel();
|
|
||||||
|
|
||||||
_channel = value.channel;
|
_channel = value.channel;
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
|
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
|
||||||
_name.text = value.channel.name;
|
_name.text = value.channel.name;
|
||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
onProfileLoaded(cachedProfile, false);
|
|
||||||
if (cachedProfile.expired) {
|
|
||||||
_taskLoadProfile.run(value.channel.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadProfile.run(value.channel.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_subscription = value;
|
_subscription = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
|
||||||
val dp_55 = 55.dp(itemView.context.resources)
|
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
|
||||||
} else {
|
|
||||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
_name.text = profile.systemState.username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SubscriptionBarViewHolder";
|
private const val TAG = "SubscriptionBarViewHolder";
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||||
|
|
|
@ -11,8 +11,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.getDataLinkFromUrl
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.views.IdenticonView
|
import com.futo.platformplayer.views.IdenticonView
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class CreatorThumbnail : ConstraintLayout {
|
||||||
|
|
||||||
if (url.startsWith("polycentric://")) {
|
if (url.startsWith("polycentric://")) {
|
||||||
try {
|
try {
|
||||||
val dataLink = PolycentricCache.getDataLinkFromUrl(url)
|
val dataLink = url.getDataLinkFromUrl()
|
||||||
setHarborAvailable(true, animate, dataLink?.system);
|
setHarborAvailable(true, animate, dataLink?.system);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
setHarborAvailable(false, animate, null);
|
setHarborAvailable(false, animate, null);
|
||||||
|
|
|
@ -2,16 +2,26 @@ package com.futo.platformplayer.views.overlays
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
|
|
||||||
class QueueEditorOverlay : LinearLayout {
|
class QueueEditorOverlay : LinearLayout {
|
||||||
|
|
||||||
private val _topbar : OverlayTopbar;
|
private val _topbar : OverlayTopbar;
|
||||||
private val _editor : VideoListEditorView;
|
private val _editor : VideoListEditorView;
|
||||||
|
private val _btnSettings: ImageView;
|
||||||
|
|
||||||
|
private val _overlayContainer: FrameLayout;
|
||||||
|
|
||||||
|
|
||||||
val onClose = Event0();
|
val onClose = Event0();
|
||||||
|
|
||||||
|
@ -19,6 +29,9 @@ class QueueEditorOverlay : LinearLayout {
|
||||||
inflate(context, R.layout.overlay_queue, this)
|
inflate(context, R.layout.overlay_queue, this)
|
||||||
_topbar = findViewById(R.id.topbar);
|
_topbar = findViewById(R.id.topbar);
|
||||||
_editor = findViewById(R.id.editor);
|
_editor = findViewById(R.id.editor);
|
||||||
|
_btnSettings = findViewById(R.id.button_settings);
|
||||||
|
_overlayContainer = findViewById(R.id.overlay_container_queue);
|
||||||
|
|
||||||
|
|
||||||
_topbar.onClose.subscribe(this, onClose::emit);
|
_topbar.onClose.subscribe(this, onClose::emit);
|
||||||
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
|
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
|
||||||
|
@ -28,6 +41,10 @@ class QueueEditorOverlay : LinearLayout {
|
||||||
}
|
}
|
||||||
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
|
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
|
||||||
|
|
||||||
|
_btnSettings.setOnClickListener {
|
||||||
|
handleSettings();
|
||||||
|
}
|
||||||
|
|
||||||
_topbar.setInfo(context.getString(R.string.queue), "");
|
_topbar.setInfo(context.getString(R.string.queue), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,4 +57,8 @@ class QueueEditorOverlay : LinearLayout {
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
_topbar.onClose.remove(this);
|
_topbar.onClose.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleSettings() {
|
||||||
|
UISlideOverlays.showQueueOptionsOverlay(context, _overlayContainer);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -5,8 +5,8 @@ import android.util.AttributeSet
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.views.SupportView
|
import com.futo.platformplayer.views.SupportView
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
class SupportOverlay : LinearLayout {
|
class SupportOverlay : LinearLayout {
|
||||||
val onClose = Event0();
|
val onClose = Event0();
|
||||||
|
|
|
@ -6,9 +6,7 @@ import android.webkit.WebView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.views.SupportView
|
|
||||||
|
|
||||||
class WebviewOverlay : LinearLayout {
|
class WebviewOverlay : LinearLayout {
|
||||||
val onClose = Event0();
|
val onClose = Event0();
|
||||||
|
|
|
@ -22,12 +22,12 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|
|
@ -96,6 +96,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||||
val exoPlayerStateName: String;
|
val exoPlayerStateName: String;
|
||||||
|
|
||||||
var playing: Boolean = false;
|
var playing: Boolean = false;
|
||||||
|
val activelyPlaying: Boolean get() = (exoPlayer?.player?.playbackState == Player.STATE_READY) && (exoPlayer?.player?.playWhenReady ?: false)
|
||||||
val position: Long get() = exoPlayer?.player?.currentPosition ?: 0;
|
val position: Long get() = exoPlayer?.player?.currentPosition ?: 0;
|
||||||
val duration: Long get() = exoPlayer?.player?.duration ?: 0;
|
val duration: Long get() = exoPlayer?.player?.duration ?: 0;
|
||||||
|
|
||||||
|
@ -829,7 +830,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||||
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
|
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
|
||||||
|
|
||||||
when (error.errorCode) {
|
when (error.errorCode) {
|
||||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||||
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
|
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
|
||||||
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
|
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
|
||||||
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
|
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="100dp"
|
android:layout_height="110dp"
|
||||||
android:minHeight="0dp"
|
android:minHeight="0dp"
|
||||||
app:layout_scrollFlags="scroll"
|
app:layout_scrollFlags="scroll"
|
||||||
app:contentInsetStart="0dp"
|
app:contentInsetStart="0dp"
|
||||||
|
@ -77,7 +77,16 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="20dp"
|
android:paddingStart="20dp"
|
||||||
android:paddingEnd="20dp" />
|
android:paddingEnd="20dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_meta"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="9dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:text="0 creators" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.appcompat.widget.Toolbar>
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
|
@ -54,6 +54,22 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_export"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:contentDescription="@string/cd_button_download"
|
||||||
|
android:background="@drawable/background_button_round"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:srcCompat="@drawable/ic_export"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_share"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/button_share"
|
||||||
|
app:tint="@color/white"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_share"
|
android:id="@+id/button_share"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
|
|
|
@ -254,7 +254,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_channel_name"
|
android:id="@+id/text_channel_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
@ -268,23 +268,10 @@
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
|
||||||
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_video_name"
|
app:layout_constraintTop_toBottomOf="@id/text_video_name"
|
||||||
android:layout_marginStart="4dp" />
|
android:layout_marginStart="4dp" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_neopass_channel"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:contentDescription="@string/neopass_channel"
|
|
||||||
app:srcCompat="@drawable/neopass"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/text_channel_name"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_video_metadata"
|
android:id="@+id/text_video_metadata"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -104,7 +104,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_channel_name"
|
android:id="@+id/text_channel_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
@ -117,21 +117,8 @@
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_playlist_name" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_neopass_channel"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:contentDescription="@string/neopass_channel"
|
|
||||||
app:srcCompat="@drawable/neopass"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/text_channel_name"
|
app:layout_constraintTop_toBottomOf="@id/text_playlist_name" />
|
||||||
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_playlist_items"
|
android:id="@+id/text_playlist_items"
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_author_name"
|
android:id="@+id/text_author_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
@ -51,24 +51,10 @@
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail"
|
||||||
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
|
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
|
||||||
tools:text="Two Minute Papers" />
|
tools:text="Two Minute Papers" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_neopass_channel"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:contentDescription="@string/neopass_channel"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/text_author_name"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/text_author_name"
|
|
||||||
app:layout_constraintRight_toLeftOf="@id/platform_indicator"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/text_author_name"
|
|
||||||
app:srcCompat="@drawable/neopass" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_metadata"
|
android:id="@+id/text_metadata"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -36,9 +36,10 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_author_name"
|
android:id="@+id/text_author_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:fontFamily="@font/inter_extra_light"
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
@ -50,24 +51,10 @@
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail"
|
||||||
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel"
|
app:layout_constraintRight_toLeftOf="@id/platform_indicator"
|
||||||
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
|
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
|
||||||
tools:text="Two Minute Papers" />
|
tools:text="Two Minute Papers" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_neopass_channel"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:contentDescription="@string/neopass_channel"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/text_author_name"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/text_author_name"
|
|
||||||
app:layout_constraintRight_toLeftOf="@id/platform_indicator"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/text_author_name"
|
|
||||||
app:srcCompat="@drawable/neopass" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_metadata"
|
android:id="@+id/text_metadata"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -232,7 +232,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_channel_name"
|
android:id="@+id/text_channel_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
@ -246,23 +246,10 @@
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
|
||||||
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_video_name"
|
app:layout_constraintTop_toBottomOf="@id/text_video_name"
|
||||||
android:layout_marginStart="4dp" />
|
android:layout_marginStart="4dp" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_neopass_channel"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:contentDescription="@string/neopass_channel"
|
|
||||||
app:srcCompat="@drawable/neopass"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/text_channel_name"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_video_metadata"
|
android:id="@+id/text_video_metadata"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -270,7 +270,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_channel_name"
|
android:id="@+id/text_channel_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
@ -284,23 +284,10 @@
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
|
||||||
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_video_name"
|
app:layout_constraintTop_toBottomOf="@id/text_video_name"
|
||||||
android:layout_marginStart="4dp" />
|
android:layout_marginStart="4dp" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image_neopass_channel"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:contentDescription="@string/neopass_channel"
|
|
||||||
app:srcCompat="@drawable/neopass"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/text_channel_name"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_video_metadata"
|
android:id="@+id/text_video_metadata"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -317,8 +304,6 @@
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
android:layout_marginStart="4dp"/>
|
android:layout_marginStart="4dp"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
android:id="@+id/thumbnail_platform_nested"
|
android:id="@+id/thumbnail_platform_nested"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
|
|
|
@ -21,5 +21,20 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/button_settings"
|
||||||
|
android:background="@drawable/background_pill"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_settings" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/overlay_container_queue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
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="also_removes_any_data_related_plugin_like_login_or_settings">Also removes any data related plugin like login or settings</string>
|
||||||
<string name="announcement">Announcement</string>
|
<string name="announcement">Announcement</string>
|
||||||
<string name="notifications">Notifications</string>
|
<string name="notifications">Notifications</string>
|
||||||
|
<string name="check_disabled_plugin_updates">Check disabled plugins for updates</string>
|
||||||
|
<string name="check_disabled_plugin_updates_description">Check disabled plugins for updates</string>
|
||||||
<string name="planned_content_notifications">Planned Content Notifications</string>
|
<string name="planned_content_notifications">Planned Content Notifications</string>
|
||||||
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
|
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
|
||||||
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
||||||
|
@ -416,7 +418,7 @@
|
||||||
<string name="log_level">Log Level</string>
|
<string name="log_level">Log Level</string>
|
||||||
<string name="logging">Logging</string>
|
<string name="logging">Logging</string>
|
||||||
<string name="sync_grayjay">Sync Grayjay</string>
|
<string name="sync_grayjay">Sync Grayjay</string>
|
||||||
<string name="sync_grayjay_description">Sync your settings across multiple devices</string>
|
<string name="sync_grayjay_description">Sync your data across multiple devices</string>
|
||||||
<string name="manage_polycentric_identity">Manage Polycentric identity</string>
|
<string name="manage_polycentric_identity">Manage Polycentric identity</string>
|
||||||
<string name="manage_your_polycentric_identity">Manage your Polycentric identity</string>
|
<string name="manage_your_polycentric_identity">Manage your Polycentric identity</string>
|
||||||
<string name="manual_check">Manual check</string>
|
<string name="manual_check">Manual check</string>
|
||||||
|
@ -445,6 +447,8 @@
|
||||||
<string name="preferred_preview_quality">Preferred Preview Quality</string>
|
<string name="preferred_preview_quality">Preferred Preview Quality</string>
|
||||||
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
|
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
|
||||||
<string name="primary_language">Primary Language</string>
|
<string name="primary_language">Primary Language</string>
|
||||||
|
<string name="prefer_original_audio">Prefer Original Audio</string>
|
||||||
|
<string name="prefer_original_audio_description">Use original audio instead of preferred language when it is known</string>
|
||||||
<string name="default_comment_section">Default Comment Section</string>
|
<string name="default_comment_section">Default Comment Section</string>
|
||||||
<string name="hide_recommendations">Hide Recommendations</string>
|
<string name="hide_recommendations">Hide Recommendations</string>
|
||||||
<string name="hide_recommendations_description">Fully hide the recommendations tab.</string>
|
<string name="hide_recommendations_description">Fully hide the recommendations tab.</string>
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit f79c7141bcb11464103abc56fd7be492fe8568ab
|
|
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