Compare commits

...

14 Commits

Author SHA1 Message Date
Koen J c813fb4fad Do not show history entries where it could not retrieve a plugin id. 2025-05-07 14:34:57 +02:00
Koen J bf7001b578 Implemented to background button in pip. 2025-05-07 14:17:29 +02:00
Koen J 18102a2a73 Try a more heavy handed approach to get plugin ids for history changes. 2025-05-07 13:26:31 +02:00
Koen J 780c1dbde1 Reverted platform filters on history page. 2025-05-07 13:18:22 +02:00
Koen J 879aab0d99 Added progress bar to playlist items. 2025-05-07 13:08:13 +02:00
Koen J 6f37bc2f5d Confirming sync now brings you back to the device list. 2025-05-07 12:57:44 +02:00
Koen J fc59b841d6 History now filters out videos of plugins that are not enabled. History now has a list of filters to filter specific plugins. History now shows an icon of which platform a specific history video is on. 2025-05-07 12:49:39 +02:00
Koen J c07fcdd489 Prevent going into picture in picture when clicking add sources. 2025-05-07 11:22:54 +02:00
Koen J a49db10ade Fixed issue 2163. 2025-05-07 11:01:25 +02:00
Koen J 77bae98d77 Private mode visibility no longer overlays pip/minimized video. 2025-05-07 10:57:39 +02:00
Koen J 254df7211c Fixed datetime checking related to playlists on android. 2025-05-07 10:36:32 +02:00
Koen J f9caab48c4 Added insensitivity to base64 formats. 2025-05-07 10:12:12 +02:00
Koen J e0b5e7b808 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-05-06 16:55:51 +02:00
Koen J ac3a8da002 Various fixes for android to android pairing. 2025-05-06 16:54:58 +02:00
18 changed files with 254 additions and 67 deletions
@@ -7,6 +7,9 @@ import java.net.InetAddress
import java.net.URI
import java.net.URISyntaxException
import java.net.URLEncoder
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
//Syntax sugaring
inline fun <reified T> Any.assume(): T?{
@@ -50,4 +53,20 @@ fun InetAddress?.toUrlAddress(): String {
throw Exception("Invalid address type")
}
}
}
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
}
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
}
@@ -186,6 +186,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _isVisible = true;
private var _wasStopped = false;
private var _privateModeEnabled = false
private var _pictureInPictureEnabled = false
private var _isFullscreen = false
private var _isMinimized = false
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@@ -363,14 +367,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_buttonIncognito.alpha = 0f;
StateApp.instance.privateModeChanged.subscribe {
//Messing with visibility causes some issues with layout ordering?
if (it) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
_privateModeEnabled = it
updatePrivateModeVisibility()
}
_buttonIncognito.setOnClickListener {
if (!StateApp.instance.privateMode)
return@setOnClickListener;
@@ -387,19 +387,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
};
_fragVideoDetail.onFullscreenChanged.subscribe {
Logger.i(TAG, "onFullscreenChanged ${it}");
_isFullscreen = it
updatePrivateModeVisibility()
}
if (it) {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
} else {
if (StateApp.instance.privateMode) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
_fragVideoDetail.onMinimize.subscribe {
_isMinimized = true
updatePrivateModeVisibility()
}
_fragVideoDetail.onMaximized.subscribe {
_isMinimized = false
updatePrivateModeVisibility()
}
StatePlayer.instance.also {
@@ -641,6 +640,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
fun updatePrivateModeVisibility() {
if (_privateModeEnabled && !_pictureInPictureEnabled && !_isFullscreen && !_isMinimized) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
_buttonIncognito.layoutParams = _buttonIncognito.layoutParams.apply {
}
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
override fun onResume() {
super.onResume();
Logger.v(TAG, "onResume")
@@ -1064,6 +1076,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
_pictureInPictureEnabled = isInPictureInPictureMode
updatePrivateModeVisibility()
}
override fun onDestroy() {
@@ -15,6 +15,8 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.TaskHandler
@@ -22,10 +24,14 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.views.ToggleBar
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.others.TagsView
import com.futo.platformplayer.views.others.Toggle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -68,6 +74,8 @@ class HistoryFragment : MainFragment() {
private var _pager: IPager<HistoryVideo>? = null;
private val _results = arrayListOf<HistoryVideo>();
private var _loading = false;
private val _toggleBar: ToggleBar
private var _togglePluginsDisabled = hashSetOf<String>()
private var _automaticNextPageCounter = 0;
@@ -79,6 +87,7 @@ class HistoryFragment : MainFragment() {
_clearSearch = findViewById(R.id.button_clear_search);
_editSearch = findViewById(R.id.edit_search);
_tagsView = findViewById(R.id.tags_text);
_toggleBar = findViewById(R.id.toggle_bar)
_tagsView.setPairs(listOf(
Pair(context.getString(R.string.last_hour), 60L),
Pair(context.getString(R.string.last_24_hours), 24L * 60L),
@@ -88,6 +97,22 @@ class HistoryFragment : MainFragment() {
Pair(context.getString(R.string.all_time), -1L)
));
val toggles = StatePlatform.instance.getEnabledClients()
.filter { it is JSClient }
.map { plugin ->
val pluginName = plugin.name.lowercase()
ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) pluginName else "", plugin.icon, !_togglePluginsDisabled.contains(plugin.id), { view, active ->
if (active) {
_togglePluginsDisabled.remove(plugin.id)
} else {
_togglePluginsDisabled.add(plugin.id)
}
filtersChanged()
}).withTag("plugins")
}.toTypedArray()
_toggleBar.setToggles(*toggles)
_adapter = InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
{ _results.size },
{ view, _ ->
@@ -162,14 +187,15 @@ class HistoryFragment : MainFragment() {
else
it.nextPage();
return@TaskHandler it.getResults();
return@TaskHandler filterResults(it.getResults());
}).success {
setLoading(false);
val posBefore = _results.size;
_results.addAll(it);
_adapter.notifyItemRangeInserted(_adapter.childToParentPosition(posBefore), it.size);
ensureEnoughContentVisible(it)
val res = filterResults(it)
_results.addAll(res);
_adapter.notifyItemRangeInserted(_adapter.childToParentPosition(posBefore), res.size);
ensureEnoughContentVisible(res)
}.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
@@ -178,6 +204,10 @@ class HistoryFragment : MainFragment() {
};
}
private fun filtersChanged() {
updatePager()
}
private fun updatePager() {
val query = _editSearch.text.toString();
if (_editSearch.text.isNotEmpty()) {
@@ -246,11 +276,22 @@ class HistoryFragment : MainFragment() {
_adapter.setLoading(loading);
}
private fun filterResults(a: List<HistoryVideo>): List<HistoryVideo> {
val enabledPluginIds = StatePlatform.instance.getEnabledClients().map { it.id }.toHashSet()
val disabledPluginIds = _togglePluginsDisabled.toHashSet()
return a.filter {
val pluginId = it.video.id.pluginId ?: StatePlatform.instance.getContentClientOrNull(it.video.url)?.id ?: return@filter false
if (!enabledPluginIds.contains(pluginId))
return@filter false
return@filter !disabledPluginIds.contains(pluginId)
};
}
private fun loadPagerInternal(pager: IPager<HistoryVideo>) {
Logger.i(TAG, "Setting new internal pager on feed");
_results.clear();
val toAdd = pager.getResults();
val toAdd = filterResults(pager.getResults())
_results.addAll(toAdd);
_adapter.notifyDataSetChanged();
ensureEnoughContentVisible(toAdd)
@@ -18,6 +18,7 @@ import com.futo.platformplayer.activities.AddSourceOptionsActivity
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.stores.FragmentedStorage
@@ -44,6 +45,7 @@ class SourcesFragment : MainFragment() {
if(topBar is AddTopBarFragment) {
(topBar as AddTopBarFragment).onAdd.clear();
(topBar as AddTopBarFragment).onAdd.subscribe {
StateApp.instance.preventPictureInPicture.emit();
startActivity(Intent(requireContext(), AddSourceOptionsActivity::class.java));
};
}
@@ -93,6 +95,7 @@ class SourcesFragment : MainFragment() {
findViewById<LinearLayout>(R.id.plugin_disclaimer).isVisible = false;
}
findViewById<BigButton>(R.id.button_add_sources).onClick.subscribe {
StateApp.instance.preventPictureInPicture.emit();
fragment.startActivity(Intent(context, AddSourceOptionsActivity::class.java));
};
@@ -46,6 +46,8 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.LiveChatManager
import com.futo.platformplayer.api.media.PlatformID
@@ -571,7 +573,7 @@ class VideoDetailView : ConstraintLayout {
_player.setIsReplay(true);
val searchVideo = StatePlayer.instance.getCurrentQueueItem();
if (searchVideo is SerializedPlatformVideo?) {
if (searchVideo is SerializedPlatformVideo? && Settings.instance.playback.deleteFromWatchLaterAuto) {
searchVideo?.let { StatePlaylists.instance.removeFromWatchLater(it) };
}
@@ -688,6 +690,20 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "MediaControlReceiver.onCloseReceived")
onClose.emit()
};
MediaControlReceiver.onBackgroundReceived.subscribe(this) {
Logger.i(TAG, "MediaControlReceiver.onBackgroundReceived")
_player.switchToAudioMode();
allowBackground = true;
StateApp.instance.contextOrNull?.let {
try {
if (it is MainActivity) {
it.moveTaskToBack(true)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to move task to back", e)
}
}
};
MediaControlReceiver.onSeekToReceived.subscribe(this) { handleSeek(it); };
_container_content_description.onClose.subscribe { switchContentView(_container_content_main); };
@@ -1141,6 +1157,7 @@ class VideoDetailView : ConstraintLayout {
MediaControlReceiver.onNextReceived.remove(this);
MediaControlReceiver.onPreviousReceived.remove(this);
MediaControlReceiver.onCloseReceived.remove(this);
MediaControlReceiver.onBackgroundReceived.remove(this);
MediaControlReceiver.onSeekToReceived.remove(this);
val job = _jobHideResume;
@@ -2725,10 +2742,11 @@ class VideoDetailView : ConstraintLayout {
else
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), context.getString(R.string.play), context.getString(R.string.resumes_the_video), MediaControlReceiver.getPlayIntent(context, 6));
val toBackgroundAction = RemoteAction(Icon.createWithResource(context, R.drawable.ic_screen_share), context.getString(R.string.background), context.getString(R.string.background_switch_audio), MediaControlReceiver.getToBackgroundIntent(context, 7));
return PictureInPictureParams.Builder()
.setAspectRatio(Rational(videoSourceWidth, videoSourceHeight))
.setSourceRectHint(r)
.setActions(listOf(playpauseAction))
.setActions(listOf(toBackgroundAction, playpauseAction))
.build();
}
@@ -21,6 +21,7 @@ class MediaControlReceiver : BroadcastReceiver() {
EVENT_NEXT -> onNextReceived.emit();
EVENT_PREV -> onPreviousReceived.emit();
EVENT_CLOSE -> onCloseReceived.emit();
EVENT_BACKGROUND -> onBackgroundReceived.emit();
}
}
catch(ex: Throwable) {
@@ -38,6 +39,7 @@ class MediaControlReceiver : BroadcastReceiver() {
const val EVENT_NEXT = "Next";
const val EVENT_PREV = "Prev";
const val EVENT_CLOSE = "Close";
const val EVENT_BACKGROUND = "Background";
val onPlayReceived = Event0();
val onPauseReceived = Event0();
@@ -48,6 +50,7 @@ class MediaControlReceiver : BroadcastReceiver() {
val onLowerVolumeReceived = Event0();
val onCloseReceived = Event0()
val onBackgroundReceived = Event0()
fun getPlayIntent(context: Context, code: Int = 0) : PendingIntent = PendingIntent.getBroadcast(context, code, Intent(context, MediaControlReceiver::class.java).apply {
this.putExtra(EXTRA_MEDIA_ACTION, EVENT_PLAY);
@@ -64,5 +67,8 @@ class MediaControlReceiver : BroadcastReceiver() {
fun getCloseIntent(context: Context, code: Int = 0) : PendingIntent = PendingIntent.getBroadcast(context, code, Intent(context, MediaControlReceiver::class.java).apply {
this.putExtra(EXTRA_MEDIA_ACTION, EVENT_CLOSE);
},PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT);
fun getToBackgroundIntent(context: Context, code: Int = 0) : PendingIntent = PendingIntent.getBroadcast(context, code, Intent(context, MediaControlReceiver::class.java).apply {
this.putExtra(EXTRA_MEDIA_ACTION, EVENT_BACKGROUND);
},PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT);
}
}
@@ -1,5 +1,6 @@
package com.futo.platformplayer.serializers
import com.futo.platformplayer.sToOffsetDateTimeUTC
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@@ -37,7 +38,7 @@ class OffsetDateTimeSerializer : KSerializer<OffsetDateTime> {
return OffsetDateTime.MAX;
else if(epochSecond < -9999999999)
return OffsetDateTime.MIN;
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
return epochSecond.sToOffsetDateTimeUTC()
}
}
class OffsetDateTimeStringSerializer : KSerializer<OffsetDateTime> {
@@ -97,6 +97,7 @@ class StatePlatform {
private val _icons : HashMap<String, ImageVariable> = HashMap();
private val _iconsByName : HashMap<String, ImageVariable> = HashMap();
val hasClients: Boolean get() = _availableClients.size > 0;
@@ -192,6 +193,7 @@ class StatePlatform {
_availableClients.clear();
_icons.clear();
_iconsByName.clear()
_icons[StateDeveloper.DEV_ID] = ImageVariable(null, R.drawable.ic_security_red);
StatePlugins.instance.updateEmbeddedPlugins(context);
@@ -200,6 +202,8 @@ class StatePlatform {
for (plugin in StatePlugins.instance.getPlugins()) {
_icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
ImageVariable(plugin.config.absoluteIconUrl, null);
_iconsByName[plugin.config.name.lowercase()] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?:
ImageVariable(plugin.config.absoluteIconUrl, null);
val client = JSClient(context, plugin);
client.onCaptchaException.subscribe { c, ex ->
@@ -299,6 +303,15 @@ class StatePlatform {
return null;
}
fun getPlatformIconByName(name: String?) : ImageVariable? {
if(name == null)
return null;
val nameLower = name.lowercase()
if(_iconsByName.containsKey(nameLower))
return _iconsByName[nameLower];
return null;
}
fun setPlatformOrder(platformOrder: List<String>) {
_platformOrderPersistent.values.clear();
_platformOrderPersistent.values.addAll(platformOrder);
@@ -19,6 +19,7 @@ import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
import com.futo.platformplayer.stores.FragmentedStorage
@@ -85,7 +86,7 @@ class StatePlaylists {
if(value.isEmpty())
return OffsetDateTime.MIN;
val tryParse = value.toLongOrNull() ?: 0;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(tryParse), ZoneOffset.UTC);
return tryParse.sToOffsetDateTimeUTC();
}
private fun setWatchLaterReorderTime() {
val now = OffsetDateTime.now(ZoneOffset.UTC);
@@ -25,6 +25,7 @@ import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStringMapStorage
@@ -731,7 +732,7 @@ class StateSync {
}
for(removal in pack.groupRemovals) {
val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key);
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
val removalTime = removal.value.sToOffsetDateTimeUTC();
if(creation != null && creation.creationTime < removalTime)
StateSubscriptionGroups.instance.deleteSubscriptionGroup(removal.key, false);
}
@@ -748,7 +749,7 @@ class StateSync {
if(existing == null)
StatePlaylists.instance.createOrUpdatePlaylist(playlist, false);
else if(existing.dateUpdate.toLocalDateTime() < playlist.dateUpdate.toLocalDateTime()) {
else if(existing.dateUpdate < playlist.dateUpdate) {
existing.dateUpdate = playlist.dateUpdate;
existing.name = playlist.name;
existing.videos = playlist.videos;
@@ -759,7 +760,7 @@ class StateSync {
}
for(removal in pack.playlistRemovals) {
val creation = StatePlaylists.instance.getPlaylist(removal.key);
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC);
val removalTime = removal.value.sToOffsetDateTimeUTC();
if(creation != null && creation.dateCreation < removalTime)
StatePlaylists.instance.removePlaylist(creation, false);
@@ -777,7 +778,7 @@ class StateSync {
val allExisting = StatePlaylists.instance.getWatchLater();
for(video in pack.videos) {
val existing = allExisting.firstOrNull { it.url == video.url };
val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) OffsetDateTime.ofInstant(Instant.ofEpochSecond(pack.videoAdds[video.url] ?: 0), ZoneOffset.UTC) else OffsetDateTime.MIN;
val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN;
val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN;
if(existing == null && time > removalTime) {
StatePlaylists.instance.addToWatchLater(video, false);
@@ -788,12 +789,12 @@ class StateSync {
for(removal in pack.videoRemovals) {
val watchLater = allExisting.firstOrNull { it.url == removal.key } ?: continue;
val creation = StatePlaylists.instance.getWatchLaterRemovalTime(watchLater.url) ?: OffsetDateTime.MIN;
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value), ZoneOffset.UTC);
val removalTime = removal.value.sToOffsetDateTimeUTC()
if(creation < removalTime)
StatePlaylists.instance.removeFromWatchLater(watchLater, false, removalTime);
}
val packReorderTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pack.reorderTime), ZoneOffset.UTC);
val packReorderTime = pack.reorderTime.sToOffsetDateTimeUTC()
val localReorderTime = StatePlaylists.instance.getWatchLaterLastReorderTime();
if(localReorderTime < packReorderTime && pack.ordering != null) {
StatePlaylists.instance.updateWatchLaterOrdering(smartMerge(pack.ordering!!, StatePlaylists.instance.getWatchLaterOrdering()), true);
@@ -830,22 +831,15 @@ class StateSync {
}
}
private fun onAuthorized(remotePublicKey: String) {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized")
}
}
private fun onUnuthorized(remotePublicKey: String) {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized")
}
}
private fun createNewSyncSession(remotePublicKey: String, remoteDeviceName: String?): SyncSession {
private fun createNewSyncSession(rpk: String, remoteDeviceName: String?): SyncSession {
val remotePublicKey = rpk.base64ToByteArray().toBase64()
return SyncSession(
remotePublicKey,
onAuthorized = { it, isNewlyAuthorized, isNewSession ->
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized")
}
if (!isNewSession) {
return@SyncSession
}
@@ -857,7 +851,6 @@ class StateSync {
}
Logger.i(TAG, "$remotePublicKey authorized (name: ${it.displayName})")
onAuthorized(remotePublicKey)
_authorizedDevices.addDistinct(remotePublicKey)
_authorizedDevices.save()
deviceUpdatedOrAdded.emit(it.remotePublicKey, it)
@@ -865,10 +858,12 @@ class StateSync {
checkForSync(it);
},
onUnauthorized = {
unauthorize(remotePublicKey)
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized")
}
unauthorize(remotePublicKey)
Logger.i(TAG, "$remotePublicKey unauthorized (name: ${it.displayName})")
onUnuthorized(remotePublicKey)
synchronized(_sessions) {
it.close()
@@ -1002,6 +997,8 @@ class StateSync {
try {
syncSession.authorize()
Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation")
activity.finish()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to send authorize", e)
}
@@ -1117,10 +1114,10 @@ class StateSync {
runBlocking {
if (onStatusUpdate != null) {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate[deviceInfo.publicKey] = onStatusUpdate
_remotePendingStatusUpdate[deviceInfo.publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
}
}
relaySession.startRelayedChannel(deviceInfo.publicKey, APP_ID, deviceInfo.pairingCode)
relaySession.startRelayedChannel(deviceInfo.publicKey.base64ToByteArray().toBase64(), APP_ID, deviceInfo.pairingCode)
}
} else {
throw e
@@ -1136,7 +1133,7 @@ class StateSync {
val session = createSocketSession(socket, false)
if (onStatusUpdate != null) {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate[publicKey] = onStatusUpdate
_remotePendingStatusUpdate[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
}
}
@@ -5,6 +5,8 @@ import com.futo.platformplayer.noise.protocol.CipherStatePair
import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.HandshakeState
import com.futo.platformplayer.states.StateSync
import com.futo.polycentric.core.base64ToByteArray
import com.futo.polycentric.core.toBase64
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
@@ -82,7 +84,7 @@ class ChannelRelayed(
override var authorizable: IAuthorizable? = null
val isAuthorized: Boolean get() = authorizable?.isAuthorized ?: false
var connectionId: Long = 0L
override var remotePublicKey: String? = publicKey
override var remotePublicKey: String? = publicKey.base64ToByteArray().toBase64()
private set
override var remoteVersion: Int? = null
private set
@@ -11,6 +11,8 @@ import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.HandshakeState
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.ChannelRelayed.Companion
import com.futo.polycentric.core.base64ToByteArray
import com.futo.polycentric.core.toBase64
import kotlinx.coroutines.CompletableDeferred
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -169,7 +171,7 @@ class SyncSocketSession {
var totalBytesReceived: Int = 0
while (totalBytesReceived < size) {
val bytesReceived = _inputStream.read(buffer, offset + totalBytesReceived, size - totalBytesReceived)
if (bytesReceived == 0)
if (bytesReceived <= 0)
throw Exception("Socket disconnected")
totalBytesReceived += bytesReceived
}
@@ -291,7 +293,7 @@ class SyncSocketSession {
_cipherStatePair = initiator.split()
val remoteKeyBytes = ByteArray(initiator.remotePublicKey.publicKeyLength)
initiator.remotePublicKey.getPublicKey(remoteKeyBytes, 0)
_remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes)
_remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes).base64ToByteArray().toBase64()
}
private fun handshakeAsResponder(): Boolean {
@@ -345,7 +347,7 @@ class SyncSocketSession {
_outputStream.write(responseBuffer, 0, 4 + responseLength)
_cipherStatePair = responder.split()
_remotePublicKey = remotePublicKey
_remotePublicKey = remotePublicKey.base64ToByteArray().toBase64()
return true
}
@@ -440,7 +442,7 @@ class SyncSocketSession {
ByteBuffer.wrap(_sendBufferEncrypted, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(len)
_outputStream.write(_sendBufferEncrypted, 0, 4 + len)
}
//Logger.v(TAG, "_outputStream.write (opcode: ${opcode}, subOpcode: ${subOpcode}, processedData.remaining(): ${processedData.remaining()}, sendDuration: ${sendDuration})")
Logger.v(TAG, "_outputStream.write (opcode: ${opcode}, subOpcode: ${subOpcode}, processedData.remaining(): ${processedData.remaining()}, sendDuration: ${sendDuration})")
}
}
}
@@ -840,11 +842,14 @@ class SyncSocketSession {
if (!isGzipSupported)
throw Exception("Failed to handle packet, gzip is not supported for this opcode (opcode = ${opcode}, subOpcode = ${subOpcode}, data.length = ${data.remaining()}).")
val compressedStream = ByteArrayInputStream(data.array(), data.position(), data.remaining());
var outputStream = ByteArrayOutputStream();
val compressedStream = ByteArrayInputStream(data.array(), data.position(), data.remaining())
val outputStream = ByteArrayOutputStream()
GZIPInputStream(compressedStream).use { gzipStream ->
gzipStream.copyToOutputStream(outputStream);
gzipStream.close();
val buffer = ByteArray(8192) // 8KB buffer
var bytesRead: Int
while (gzipStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
data = ByteBuffer.wrap(outputStream.toByteArray())
}
@@ -933,7 +938,7 @@ class SyncSocketSession {
throw Exception("After sync stream end, the stream must be complete")
}
handlePacket(syncStream.opcode, syncStream.subOpcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) }, contentEncoding, sourceChannel)
handlePacket(syncStream.opcode, syncStream.subOpcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) }, syncStream.contentEncoding, sourceChannel)
}
}
Opcode.DATA.value -> {
@@ -993,7 +998,7 @@ class SyncSocketSession {
suspend fun startRelayedChannel(publicKey: String, appId: UInt = 0u, pairingCode: String? = null): ChannelRelayed? {
val requestId = generateRequestId()
val deferred = CompletableDeferred<ChannelRelayed>()
val channel = ChannelRelayed(this, _localKeyPair, publicKey, true)
val channel = ChannelRelayed(this, _localKeyPair, publicKey.base64ToByteArray().toBase64(), true)
_onNewChannel?.invoke(this, channel)
_pendingChannels[requestId] = channel to deferred
try {
@@ -14,9 +14,11 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.platform.PlatformIndicator
class HistoryListViewHolder : ViewHolder {
private val _root: ConstraintLayout;
@@ -30,6 +32,7 @@ class HistoryListViewHolder : ViewHolder {
private val _imageRemove: ImageButton;
private val _textHeader: TextView;
private val _timeBar: ProgressBar;
private val _thumbnailPlatform: PlatformIndicator
var video: HistoryVideo? = null
private set;
@@ -47,6 +50,7 @@ class HistoryListViewHolder : ViewHolder {
_textVideoDuration = itemView.findViewById(R.id.thumbnail_duration);
_containerDuration = itemView.findViewById(R.id.thumbnail_duration_container);
_containerLive = itemView.findViewById(R.id.thumbnail_live_container);
_thumbnailPlatform = itemView.findViewById(R.id.thumbnail_platform)
_imageRemove = itemView.findViewById(R.id.image_trash);
_textHeader = itemView.findViewById(R.id.text_header);
_timeBar = itemView.findViewById(R.id.time_bar);
@@ -73,6 +77,9 @@ class HistoryListViewHolder : ViewHolder {
_textAuthor.text = v.video.author.name;
_textVideoDuration.text = v.video.duration.toHumanTime(false);
val pluginId = v.video.id.pluginId ?: StatePlatform.instance.getContentClientOrNull(v.video.url)?.id
_thumbnailPlatform.setPlatformFromClientID(pluginId)
if(v.video.isLive) {
_containerDuration.visibility = View.GONE;
_containerLive.visibility = View.VISIBLE;
@@ -17,9 +17,11 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.platform.PlatformIndicator
class VideoListEditorViewHolder : ViewHolder {
@@ -36,6 +38,7 @@ class VideoListEditorViewHolder : ViewHolder {
private val _imageDragDrop: ImageButton;
private val _platformIndicator: PlatformIndicator;
private val _layoutDownloaded: FrameLayout;
private val _timeBar: ProgressBar
var video: IPlatformVideo? = null
private set;
@@ -59,6 +62,7 @@ class VideoListEditorViewHolder : ViewHolder {
_imageOptions = view.findViewById(R.id.image_settings);
_imageDragDrop = view.findViewById<ImageButton>(R.id.image_drag_drop);
_platformIndicator = view.findViewById(R.id.thumbnail_platform);
_timeBar = view.findViewById(R.id.time_bar);
_layoutDownloaded = view.findViewById(R.id.layout_downloaded);
_imageDragDrop.setOnTouchListener { _, event ->
@@ -93,6 +97,9 @@ class VideoListEditorViewHolder : ViewHolder {
_textAuthor.text = v.author.name;
_textVideoDuration.text = v.duration.toHumanTime(false);
val historyPosition = StateHistory.instance.getHistoryPosition(v.url)
_timeBar.progress = historyPosition.toFloat() / v.duration.toFloat();
if(v.isLive) {
_containerDuration.visibility = View.GONE;
_containerLive.visibility = View.VISIBLE;
@@ -22,4 +22,15 @@ class PlatformIndicator : androidx.appcompat.widget.AppCompatImageView {
setImageResource(0);
}
}
fun setPlatformFromClientName(name: String?) {
if(name == null)
setImageResource(0);
else {
val result = StatePlatform.instance.getPlatformIconByName(name);
if (result != null)
result.setImageView(this);
else
setImageResource(0);
}
}
}
@@ -94,6 +94,25 @@
android:id="@+id/tags_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text_filters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/filters"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:fontFamily="@font/inter_light"
android:textSize="16dp"
android:textColor="@color/white"
android:paddingStart="5dp"
android:paddingTop="15dp"
android:paddingBottom="8dp" />
<com.futo.platformplayer.views.ToggleBar
android:id="@+id/toggle_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
+9
View File
@@ -117,6 +117,15 @@
app:radiusBottomRight="4dp"
app:radiusTopLeft="0dp"
app:radiusTopRight="0dp" />
<com.futo.platformplayer.views.platform.PlatformIndicator
android:id="@+id/thumbnail_platform"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@string/cd_platform_indicator"
android:layout_gravity="bottom|start"
android:layout_marginStart="4dp"
android:layout_marginBottom="4dp" />
</FrameLayout>
<TextView
+16 -3
View File
@@ -41,6 +41,19 @@
app:srcCompat="@drawable/placeholder_video_thumbnail"
android:background="@drawable/video_thumbnail_outline" />
<com.futo.platformplayer.views.others.ProgressBar
android:id="@+id/time_bar"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_gravity="bottom"
app:progress="60%"
app:inactiveColor="#55EEEEEE"
android:layout_marginBottom="0dp"
app:radiusBottomLeft="4dp"
app:radiusBottomRight="4dp"
app:radiusTopLeft="0dp"
app:radiusTopRight="0dp" />
<LinearLayout
android:id="@+id/thumbnail_live_container"
android:layout_width="wrap_content"
@@ -49,7 +62,7 @@
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:layout_marginBottom="6dp"
android:paddingTop="0dp"
android:gravity="center_vertical"
android:orientation="horizontal"
@@ -77,7 +90,7 @@
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:layout_marginBottom="6dp"
android:paddingTop="0dp"
android:gravity="center_vertical"
android:orientation="horizontal"
@@ -103,7 +116,7 @@
android:layout_height="20dp"
android:layout_gravity="bottom|start"
android:layout_marginStart="4dp"
android:layout_marginBottom="4dp" />
android:layout_marginBottom="6dp" />
<FrameLayout android:id="@+id/layout_downloaded"
android:layout_width="16dp"