Deleting playlist video deletes local files, Post links are now clickable, going back to channel page from post now shows channel page correctly, search capabilities correct for channel content search, Fix loader not disappearing in certain cases on post details

This commit is contained in:
Kelvin 2024-04-16 21:15:28 +02:00
parent b370af9d91
commit 66c7741c38
13 changed files with 103 additions and 17 deletions

View File

@ -788,8 +788,8 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() }; return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
} }
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters { fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues); val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
overlay.show(); overlay.show();
return overlay; return overlay;
} }

View File

@ -13,6 +13,7 @@ data class PlatformClientCapabilities(
val hasGetChannelUrlByClaim: Boolean = false, val hasGetChannelUrlByClaim: Boolean = false,
val hasGetChannelTemplateByClaimMap: Boolean = false, val hasGetChannelTemplateByClaimMap: Boolean = false,
val hasGetSearchCapabilities: Boolean = false, val hasGetSearchCapabilities: Boolean = false,
val hasGetSearchChannelContentsCapabilities: Boolean = false,
val hasGetChannelCapabilities: Boolean = false, val hasGetChannelCapabilities: Boolean = false,
val hasGetLiveEvents: Boolean = false, val hasGetLiveEvents: Boolean = false,
val hasGetLiveChatWindow: Boolean = false, val hasGetLiveChatWindow: Boolean = false,

View File

@ -216,6 +216,7 @@ open class JSClient : IPlatformClient {
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false, hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false, hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false, hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false, hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false, hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false, hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
@ -308,6 +309,9 @@ open class JSClient : IPlatformClient {
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos") @JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
override fun getSearchChannelContentsCapabilities(): ResultCapabilities { override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
if(!capabilities.hasGetSearchChannelContentsCapabilities)
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
ensureEnabled(); ensureEnabled();
if (_searchChannelContentsCapabilities != null) if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!; return _searchChannelContentsCapabilities!!;

View File

@ -34,6 +34,20 @@ class PackageUtilities : V8Package {
fun md5(arr: ByteArray): ByteArray { fun md5(arr: ByteArray): ByteArray {
return MessageDigest.getInstance("MD5").digest(arr); return MessageDigest.getInstance("MD5").digest(arr);
} }
@V8Function
fun md5String(str: String): String {
return md5(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
}
@V8Function
fun sha256(arr: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(arr);
}
@V8Function
fun sha256String(str: String): String {
return sha256(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
}
@V8Function @V8Function
fun randomUUID(): String { fun randomUUID(): String {

View File

@ -271,7 +271,7 @@ class ChannelFragment : MainFragment() {
_taskLoadPolycentricProfile.cancel(); _taskLoadPolycentricProfile.cancel();
_selectedTabIndex = -1; _selectedTabIndex = -1;
if (!isBack) { if (!isBack || _url == null) {
_imageBanner.setImageDrawable(null); _imageBanner.setImageDrawable(null);
if (parameter is String) { if (parameter is String) {

View File

@ -1,6 +1,7 @@
package com.futo.platformplayer.fragment.mainactivity.main package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Browser import android.provider.Browser
import android.view.LayoutInflater import android.view.LayoutInflater
@ -220,8 +221,10 @@ class CommentsFragment : MainFragment() {
Logger.i(TAG, "onAuthorClick: " + c.author.id.value); Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
if(c.author.id.value?.startsWith("polycentric://") ?: false) { if(c.author.id.value?.startsWith("polycentric://") ?: false) {
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length); //val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
_fragment.navigate<BrowserFragment>(navUrl); val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
//_fragment.navigate<BrowserFragment>(navUrl);
} }
} }

View File

@ -129,7 +129,7 @@ class ContentSearchResultsFragment : MainFragment() {
onFilterClick.subscribe(this) { onFilterClick.subscribe(this) {
_overlayContainer.let { _overlayContainer.let {
val filterValuesCopy = HashMap(_filterValues); val filterValuesCopy = HashMap(_filterValues);
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy); val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy, _channelUrl != null);
filtersOverlay.onOK.subscribe { enabledClientIds, changed -> filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
if (changed) { if (changed) {
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy); setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
@ -170,7 +170,11 @@ class ContentSearchResultsFragment : MainFragment() {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
val commonCapabilities = StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id }); val commonCapabilities =
if(_channelUrl == null)
StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
else
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
val sorts = commonCapabilities?.sorts ?: listOf(); val sorts = commonCapabilities?.sorts ?: listOf();
if (sorts.size > 1) { if (sorts.size > 1) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@ -35,6 +35,7 @@ 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.PolycentricCache
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
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
@ -211,6 +212,8 @@ class PostDetailFragment : MainFragment {
_repliesOverlay = findViewById(R.id.replies_overlay); _repliesOverlay = findViewById(R.id.replies_overlay);
_textContent.setPlatformPlayerLinkMovementMethod(context);
_buttonSubscribe.onSubscribed.subscribe { _buttonSubscribe.onSubscribed.subscribe {
//TODO: add overlay to layout //TODO: add overlay to layout
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); //UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);

View File

@ -656,11 +656,11 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "onAuthorClick: " + c.author.id.value); Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
if(c.author.id.value?.startsWith("polycentric://") ?: false) { if(c.author.id.value?.startsWith("polycentric://") ?: false) {
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length); //val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
//fragment.navigate<BrowserFragment>(navUrl); val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
//fragment.minimizeVideoDetail(); fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
_container_content_browser.goto(navUrl); //_container_content_browser.goto(navUrl);
switchContentView(_container_content_browser); //switchContentView(_container_content_browser);
} }
}; };
_commentsList.onRepliesClick.subscribe { c -> _commentsList.onRepliesClick.subscribe { c ->

View File

@ -144,10 +144,15 @@ class StateDownloads {
fun getDownloadedVideos(): List<VideoLocal> { fun getDownloadedVideos(): List<VideoLocal> {
return _downloaded.getItems(); return _downloaded.getItems();
} }
fun getDownloadedVideosPlaylist(str: String): List<VideoLocal> {
val videos = _downloaded.findItems { it.groupID == str };
return videos;
}
fun getDownloadPlaylists(): List<PlaylistDownloadDescriptor> { fun getDownloadPlaylists(): List<PlaylistDownloadDescriptor> {
return _downloadPlaylists.getItems(); return _downloadPlaylists.getItems();
} }
fun isPlaylistCached(id: String): Boolean { fun isPlaylistCached(id: String): Boolean {
return getDownloadPlaylists().any{it.id == id}; return getDownloadPlaylists().any{it.id == id};
} }
@ -188,6 +193,21 @@ class StateDownloads {
DownloadService.getOrCreateService(it); DownloadService.getOrCreateService(it);
} }
} }
fun checkForOutdatedPlaylistVideos(playlistId: String) {
val playlistVideos = if(playlistId == VideoDownload.GROUP_WATCHLATER)
(if(getWatchLaterDescriptor() != null) StatePlaylists.instance.getWatchLater() else listOf())
else
getCachedPlaylist(playlistId)?.playlist?.videos ?: return;
val playlistVideosDownloaded = getDownloadedVideosPlaylist(playlistId);
val urls = playlistVideos.map { it.url }.toHashSet();
for(item in playlistVideosDownloaded) {
if(!urls.contains(item.url)) {
Logger.i(TAG, "Playlist [${playlistId}] deleting removed video [${item.name}]");
deleteCachedVideo(item.id);
}
}
}
fun checkForOutdatedPlaylists(): Boolean { fun checkForOutdatedPlaylists(): Boolean {
var hasChanged = false; var hasChanged = false;
val playlistsDownloaded = getCachedPlaylists(); val playlistsDownloaded = getCachedPlaylists();
@ -230,7 +250,7 @@ class StateDownloads {
else { else {
Logger.i(TAG, "New watchlater video ${item.name}"); Logger.i(TAG, "New watchlater video ${item.name}");
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate) download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
.withGroup(VideoDownload.GROUP_PLAYLIST, VideoDownload.GROUP_WATCHLATER), false); .withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
hasNew = true; hasNew = true;
} }
} }

View File

@ -529,12 +529,23 @@ class StatePlatform {
} }
fun getCommonSearchCapabilities(clientIds: List<String>): ResultCapabilities? { fun getCommonSearchCapabilities(clientIds: List<String>): ResultCapabilities? {
return getCommonSearchCapabilitiesType(clientIds){
it.getSearchCapabilities()
};
}
fun getCommonSearchChannelContentsCapabilities(clientIds: List<String>): ResultCapabilities? {
return getCommonSearchCapabilitiesType(clientIds){
it.getSearchChannelContentsCapabilities()
};
}
fun getCommonSearchCapabilitiesType(clientIds: List<String>, capabilitiesGetter: (client: IPlatformClient)-> ResultCapabilities): ResultCapabilities? {
try { try {
Logger.i(TAG, "Platform - getCommonSearchCapabilities"); Logger.i(TAG, "Platform - getCommonSearchCapabilities");
val clients = getEnabledClients().filter { clientIds.contains(it.id) }; val clients = getEnabledClients().filter { clientIds.contains(it.id) };
val c = clients.firstOrNull() ?: return null; val c = clients.firstOrNull() ?: return null;
val cap = c.getSearchCapabilities(); val cap = capabilitiesGetter(c)//c.getSearchCapabilities();
//var types = arrayListOf<String>(); //var types = arrayListOf<String>();
var sorts = cap.sorts.toMutableList(); var sorts = cap.sorts.toMutableList();
@ -544,7 +555,7 @@ class StatePlatform {
val filtersToRemove = arrayListOf<Int>(); val filtersToRemove = arrayListOf<Int>();
for (i in 1 until clients.size) { for (i in 1 until clients.size) {
val clientSearchCapabilities = clients[i].getSearchCapabilities(); val clientSearchCapabilities = capabilitiesGetter(clients[i]);//.getSearchCapabilities();
for (j in 0 until sorts.size) { for (j in 0 until sorts.size) {
if (!clientSearchCapabilities.sorts.contains(sorts[j])) { if (!clientSearchCapabilities.sorts.contains(sorts[j])) {

View File

@ -11,6 +11,7 @@ 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.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.ReconstructionException import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -66,6 +67,10 @@ class StatePlaylists {
_watchlistOrderStore.save(); _watchlistOrderStore.save();
} }
onWatchLaterChanged.emit(); onWatchLaterChanged.emit();
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
}
} }
fun removeFromWatchLater(video: SerializedPlatformVideo) { fun removeFromWatchLater(video: SerializedPlatformVideo) {
synchronized(_watchlistStore) { synchronized(_watchlistStore) {
@ -74,6 +79,10 @@ class StatePlaylists {
_watchlistOrderStore.save(); _watchlistOrderStore.save();
} }
onWatchLaterChanged.emit(); onWatchLaterChanged.emit();
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
}
} }
fun addToWatchLater(video: SerializedPlatformVideo) { fun addToWatchLater(video: SerializedPlatformVideo) {
synchronized(_watchlistStore) { synchronized(_watchlistStore) {
@ -82,6 +91,8 @@ class StatePlaylists {
_watchlistOrderStore.save(); _watchlistOrderStore.save();
} }
onWatchLaterChanged.emit(); onWatchLaterChanged.emit();
StateDownloads.instance.checkForOutdatedPlaylists();
} }
fun getLastPlayedPlaylist() : Playlist? { fun getLastPlayedPlaylist() : Playlist? {
@ -131,6 +142,11 @@ class StatePlaylists {
fun createOrUpdatePlaylist(playlist: Playlist) { fun createOrUpdatePlaylist(playlist: Playlist) {
playlist.dateUpdate = OffsetDateTime.now(); playlist.dateUpdate = OffsetDateTime.now();
playlistStore.saveAsync(playlist, true); playlistStore.saveAsync(playlist, true);
if(playlist.id.isNotEmpty()) {
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
}
}
} }
fun addToPlaylist(id: String, video: IPlatformVideo) { fun addToPlaylist(id: String, video: IPlatformVideo) {
synchronized(playlistStore) { synchronized(playlistStore) {
@ -143,6 +159,9 @@ class StatePlaylists {
fun removePlaylist(playlist: Playlist) { fun removePlaylist(playlist: Playlist) {
playlistStore.delete(playlist); playlistStore.delete(playlist);
if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
}
} }
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri { fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {

View File

@ -28,13 +28,17 @@ class SlideUpMenuFilters {
private var _changed: Boolean = false; private var _changed: Boolean = false;
private val _lifecycleScope: CoroutineScope; private val _lifecycleScope: CoroutineScope;
private var _isChannelSearch = false;
var commonCapabilities: ResultCapabilities? = null; var commonCapabilities: ResultCapabilities? = null;
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>) {
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false) {
_lifecycleScope = lifecycleScope; _lifecycleScope = lifecycleScope;
_container = container; _container = container;
_enabledClientsIds = enabledClientsIds; _enabledClientsIds = enabledClientsIds;
_filterValues = filterValues; _filterValues = filterValues;
_isChannelSearch = isChannelSearch;
_slideUpMenuOverlay = SlideUpMenuOverlay(_container.context, _container, container.context.getString(R.string.filters), container.context.getString(R.string.done), true, listOf()); _slideUpMenuOverlay = SlideUpMenuOverlay(_container.context, _container, container.context.getString(R.string.filters), container.context.getString(R.string.done), true, listOf());
_slideUpMenuOverlay.onOK.subscribe { _slideUpMenuOverlay.onOK.subscribe {
onOK.emit(_enabledClientsIds, _changed); onOK.emit(_enabledClientsIds, _changed);
@ -47,7 +51,10 @@ class SlideUpMenuFilters {
private fun updateCommonCapabilities() { private fun updateCommonCapabilities() {
_lifecycleScope.launch(Dispatchers.IO) { _lifecycleScope.launch(Dispatchers.IO) {
try { try {
val caps = StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds); val caps = if(!_isChannelSearch)
StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds);
else
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(_enabledClientsIds);
synchronized(_filterValues) { synchronized(_filterValues) {
if (caps != null) { if (caps != null) {
val keysToRemove = arrayListOf<String>(); val keysToRemove = arrayListOf<String>();