From c6cd7110ca297bfb9f74147fcca97513cb9b2863 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Oct 2025 14:16:40 -0500 Subject: [PATCH 1/2] new button to sync imported playlist --- .../platforms/js/models/JSPlaylistDetails.kt | 2 +- .../mainactivity/main/PlaylistFragment.kt | 110 ++++++++++++++++++ .../mainactivity/main/VideoListEditorView.kt | 20 ++++ .../futo/platformplayer/models/Playlist.kt | 8 +- .../platformplayer/states/StatePlaylists.kt | 4 +- .../platformplayer/stores/v2/ManagedStore.kt | 7 ++ app/src/main/res/drawable/ic_sync.xml | 9 ++ .../res/layout/fragment_video_list_editor.xml | 19 ++- app/src/main/res/values/strings.xml | 1 + 9 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/ic_sync.xml diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt index 787dacf28..8f6780fc6 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt @@ -38,6 +38,6 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails { onProgress?.invoke(videos.size); } - return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)}); + return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)}, url); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt index 1e90e36ec..ef7d514ab 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.* import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist +import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.constructs.TaskHandler @@ -22,11 +23,13 @@ import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists +import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class PlaylistFragment : MainFragment() { override val isMainView : Boolean = true; @@ -112,6 +115,113 @@ class PlaylistFragment : MainFragment() { _editPlaylistOverlay = editPlaylistOverlay; _editPlaylistNameInput = nameInput; + setOnSync { + val playlist = _playlist ?: return@setOnSync; + if (playlist.url.isNullOrBlank()) { + UIDialogs.appToast("Can't sync playlist because it was not imported", false); + } + else { + UISlideOverlays.showOverlay(overlayContainer, "Sync " + context.getString(R.string.playlist) + " [${playlist.name}] with source", null, {}, + SlideUpMenuItem( + context, + R.drawable.ic_sync, + "Keep videos not on source", + "", + tag = 1, + call = { + val taskLoadPlaylist = TaskHandler({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link); }) + .success { + if (it != null) { + val playlistToImport = SelectablePlaylist(it); + + UIDialogs.showDialogProgress(context) { + it.setText("Syncing playlist.."); + it.setProgress(0f); + _fragment.lifecycleScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + it.setText("Syncing playlist..\n[${playlistToImport.playlist.name}]"); + } + try { + val tempPlaylist = playlistToImport.playlist.toPlaylist(); + tempPlaylist.id = playlist.id; + tempPlaylist.videos = (tempPlaylist.videos + playlist.videos) + .distinctBy { it.id } as java.util.ArrayList; + StatePlaylists.instance.createOrUpdatePlaylist(tempPlaylist, true, true); + playlist.videos = tempPlaylist.videos; + } + catch(ex: Throwable) { + UIDialogs.appToast("Failed to sync [${playlistToImport.playlist.name}]\n" + ex.message); + } + + withContext(Dispatchers.Main) { + UIDialogs.toast("${playlist.name} " + "Synced"); + _fragment.closeSegment(); + it.dismiss(); + } + } + } + + } + + onShown(playlist) + + }.exceptionWithParameter { ex, para -> + Logger.w(ChannelFragment.TAG, "Failed to load results.", ex); + UIDialogs.appToast(context.getString(R.string.failed_to_fetch) + "\n${para}\n" + ex.message, false) + }; + taskLoadPlaylist.run(playlist.url); + }), + SlideUpMenuItem( + context, + R.drawable.ic_sync, + "Remove videos not on source", + "", + tag = 2, + call = { + val taskLoadPlaylist = TaskHandler({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link); }) + .success { + if (it != null) { + val playlistToImport = SelectablePlaylist(it); + + UIDialogs.showDialogProgress(context) { + it.setText("Syncing playlist.."); + it.setProgress(0f); + _fragment.lifecycleScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + it.setText("Syncing playlist..\n[${playlistToImport.playlist.name}]"); + } + try { + val tempPlaylist = playlistToImport.playlist.toPlaylist(); + tempPlaylist.id = playlist.id; + StatePlaylists.instance.createOrUpdatePlaylist(tempPlaylist, true, true); + playlist.videos = tempPlaylist.videos; + } + catch(ex: Throwable) { + UIDialogs.appToast("Failed to sync [${playlistToImport.playlist.name}]\n" + ex.message); + } + + withContext(Dispatchers.Main) { + UIDialogs.toast("${playlist.name} " + "Synced"); + _fragment.closeSegment(); + it.dismiss(); + } + } + } + + } + + onShown(playlist) + + }.exceptionWithParameter { ex, para -> + Logger.w(ChannelFragment.TAG, "Failed to load results.", ex); + UIDialogs.appToast(context.getString(R.string.failed_to_fetch) + "\n${para}\n" + ex.message, false) + }; + taskLoadPlaylist.run(playlist.url); + }) + ); + } + }; + setOnShare { val playlist = _playlist ?: return@setOnShare; val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt index 10deee40a..f72ede403 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt @@ -42,9 +42,11 @@ abstract class VideoListEditorView : LinearLayout { private var _buttonShare: ImageButton; private var _buttonEdit: ImageButton; private var _buttonSearch: ImageButton; + private var _buttonSync: ImageButton; private var _search: SearchView; + private var _onSync: (()->Unit)? = null; private var _onShare: (()->Unit)? = null; private var _loadedVideos: List? = null; @@ -68,6 +70,7 @@ abstract class VideoListEditorView : LinearLayout { val buttonPlayAll = findViewById(R.id.button_play_all); val buttonShuffle = findViewById(R.id.button_shuffle); _buttonEdit = findViewById(R.id.button_edit); + _buttonSync = findViewById(R.id.button_sync); _buttonDownload = findViewById(R.id.button_download); _buttonDownload.visibility = View.GONE; _buttonExport = findViewById(R.id.button_export); @@ -94,6 +97,14 @@ abstract class VideoListEditorView : LinearLayout { } } + val onSync = _onSync; + if(onSync != null) { + _buttonSync.setOnClickListener { hideSearchKeyboard(); onSync.invoke() }; + _buttonSync.visibility = View.VISIBLE; + } + else + _buttonSync.visibility = View.GONE; + _buttonShare = findViewById(R.id.button_share); val onShare = _onShare; if(onShare != null) { @@ -118,6 +129,15 @@ abstract class VideoListEditorView : LinearLayout { _videoListEditorView = videoListEditorView; } + fun setOnSync(onSync: (()-> Unit)? = null) { + _onSync = onSync; + _buttonSync.setOnClickListener { + hideSearchKeyboard(); + onSync?.invoke(); + }; + _buttonSync.visibility = View.VISIBLE; + } + fun setOnShare(onShare: (()-> Unit)? = null) { _onShare = onShare; _buttonShare.setOnClickListener { diff --git a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt index 758929d51..3ff8cc058 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt @@ -25,15 +25,19 @@ class Playlist { @kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class) var datePlayed: OffsetDateTime = OffsetDateTime.MIN; - constructor(){} + var url: String = ""; + constructor(name: String, list: List) { this.name = name; this.videos = ArrayList(list); } - constructor(id: String, name: String, list: List) { + constructor(id: String, name: String, list: List, url: String? = null) { this.id = id; this.name = name; this.videos = ArrayList(list); + if (url != null) { + this.url = url + }; } fun makeCopy(newName: String? = null): Playlist { diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt index cbe1c5186..1e25d528a 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -298,9 +298,9 @@ class StatePlaylists { createOrUpdatePlaylist(newPlaylist); return newPlaylist; } - fun createOrUpdatePlaylist(playlist: Playlist, isUserInteraction: Boolean = true) { + fun createOrUpdatePlaylist(playlist: Playlist, isUserInteraction: Boolean = true, onlyExisting: Boolean = false) { playlist.dateUpdate = OffsetDateTime.now(); - playlistStore.saveAsync(playlist, true); + playlistStore.saveAsync(playlist, true, onlyExisting); if(playlist.id.isNotEmpty()) { if (StateDownloads.instance.isPlaylistCached(playlist.id)) { StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id); diff --git a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt index 90e79ccc9..85843f72f 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.stores.v2 import com.futo.platformplayer.assume import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.ImportCache +import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.states.StateApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -263,6 +264,12 @@ class ManagedStore{ else null; var file = getFile(obj); + if (file == null && (obj as Playlist).id.isNotEmpty() && onlyExisting) { + val files = _files.filter { (key, _) -> (key as Playlist).id == (obj as Playlist).id } + if (files.size == 1) { + file = files.values.first(); + } + } if (file != null) { Logger.v(TAG, "Saving file ${logName(file.id)}"); val encoded = _serializer.serialize(_class, obj); diff --git a/app/src/main/res/drawable/ic_sync.xml b/app/src/main/res/drawable/ic_sync.xml new file mode 100644 index 000000000..9a82b0392 --- /dev/null +++ b/app/src/main/res/drawable/ic_sync.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_video_list_editor.xml b/app/src/main/res/layout/fragment_video_list_editor.xml index f86b69e4c..723a6e65d 100644 --- a/app/src/main/res/layout/fragment_video_list_editor.xml +++ b/app/src/main/res/layout/fragment_video_list_editor.xml @@ -62,7 +62,7 @@ android:gravity="center" android:layout_marginStart="5dp" android:layout_marginRight="10dp" - app:layout_constraintRight_toLeftOf="@id/button_export" + app:layout_constraintRight_toLeftOf="@id/button_sync" app:layout_constraintTop_toTopOf="@id/button_share" android:orientation="horizontal" android:scaleType="fitCenter" @@ -70,6 +70,23 @@ android:padding="10dp" app:tint="@color/white" /> + + Stop Scan QR code Help + Sync Change Polycentric profile picture Recommendations From 8b34f6ccfc96fa1049cdf591dd330c819d2564dc Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Oct 2025 14:58:34 -0500 Subject: [PATCH 2/2] validation for non playlist objects --- .../futo/platformplayer/stores/v2/ManagedStore.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt index 85843f72f..1de7ec0cc 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/v2/ManagedStore.kt @@ -264,11 +264,15 @@ class ManagedStore{ else null; var file = getFile(obj); - if (file == null && (obj as Playlist).id.isNotEmpty() && onlyExisting) { - val files = _files.filter { (key, _) -> (key as Playlist).id == (obj as Playlist).id } - if (files.size == 1) { - file = files.values.first(); + try { + if (file == null && (obj as Playlist).id.isNotEmpty() && onlyExisting) { + val files = _files.filter { (key, _) -> (key as Playlist).id == (obj as Playlist).id } + if (files.size == 1) { + file = files.values.first(); + } } + } catch (_: ClassCastException) { + // not a playlist } if (file != null) { Logger.v(TAG, "Saving file ${logName(file.id)}");