From 5dd8e6e1a66d2a4d8fd3c8ccb36ded178e747da5 Mon Sep 17 00:00:00 2001 From: James Cherished Date: Sun, 14 Dec 2025 20:38:41 +0000 Subject: [PATCH 1/2] folders for playlists --- .../org/schabi/newpipe/NewPipeDatabase.kt | 8 +- .../schabi/newpipe/database/AppDatabase.kt | 4 +- .../org/schabi/newpipe/database/Migrations.kt | 30 ++++ .../playlist/PlaylistMetadataEntry.kt | 3 + .../playlist/dao/PlaylistFolderDAO.kt | 27 ++++ .../playlist/dao/PlaylistStreamDAO.kt | 7 +- .../database/playlist/model/PlaylistEntity.kt | 9 +- .../playlist/model/PlaylistFolderEntity.kt | 30 ++++ .../local/bookmark/BookmarkFragment.java | 149 +++++++++++++++++- .../bookmark/PlaylistFoldersAdapter.java | 86 ++++++++++ .../local/playlist/LocalPlaylistManager.java | 28 ++++ .../local/playlist/PlaylistFolderManager.java | 34 ++++ .../main/res/layout/fragment_bookmarks.xml | 23 +++ app/src/main/res/layout/list_folder_item.xml | 17 ++ 14 files changed, 443 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistFolderDAO.kt create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistFolderEntity.kt create mode 100644 app/src/main/java/org/schabi/newpipe/local/bookmark/PlaylistFoldersAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/local/playlist/PlaylistFolderManager.java create mode 100644 app/src/main/res/layout/list_folder_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt index c3ce51524..6cdae7e8e 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt @@ -17,6 +17,7 @@ import org.schabi.newpipe.database.Migrations.MIGRATION_5_6 import org.schabi.newpipe.database.Migrations.MIGRATION_6_7 import org.schabi.newpipe.database.Migrations.MIGRATION_7_8 import org.schabi.newpipe.database.Migrations.MIGRATION_8_9 +import org.schabi.newpipe.database.Migrations.MIGRATION_9_10 import kotlin.concurrent.Volatile object NewPipeDatabase { @@ -27,8 +28,8 @@ object NewPipeDatabase { private fun getDatabase(context: Context): AppDatabase { return databaseBuilder( context.applicationContext, - AppDatabase::class.java, - AppDatabase.Companion.DATABASE_NAME + AppDatabase::class.java, + AppDatabase.Companion.DATABASE_NAME ).addMigrations( MIGRATION_1_2, MIGRATION_2_3, @@ -37,7 +38,8 @@ object NewPipeDatabase { MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, - MIGRATION_8_9 + MIGRATION_8_9, + MIGRATION_9_10 ).build() } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt index 286eddf7b..0bfe268f8 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt @@ -34,7 +34,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity @TypeConverters(Converters::class) @Database( - version = Migrations.DB_VER_9, + version = Migrations.DB_VER_10, entities = [ SubscriptionEntity::class, SearchHistoryEntry::class, @@ -44,6 +44,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity PlaylistEntity::class, PlaylistStreamEntity::class, PlaylistRemoteEntity::class, + org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity::class, FeedEntity::class, FeedGroupEntity::class, FeedGroupSubscriptionEntity::class, @@ -56,6 +57,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun playlistDAO(): PlaylistDAO abstract fun playlistRemoteDAO(): PlaylistRemoteDAO abstract fun playlistStreamDAO(): PlaylistStreamDAO + abstract fun playlistFolderDAO(): org.schabi.newpipe.database.playlist.dao.PlaylistFolderDAO abstract fun searchHistoryDAO(): SearchHistoryDAO abstract fun streamDAO(): StreamDAO abstract fun streamHistoryDAO(): StreamHistoryDAO diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt index 8988708e6..bf45f622c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt @@ -30,6 +30,7 @@ object Migrations { const val DB_VER_7 = 7 const val DB_VER_8 = 8 const val DB_VER_9 = 9 + const val DB_VER_10 = 10 private val TAG = Migrations::class.java.getName() private val isDebug = MainActivity.DEBUG @@ -365,4 +366,33 @@ object Migrations { } } } + + val MIGRATION_9_10 = object : Migration(DB_VER_9, DB_VER_10) { + override fun migrate(db: SupportSQLiteDatabase) { + // Add folder table and folder_id column to playlists + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlist_folders` (" + + "`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT NOT NULL, " + + "`sort_order` INTEGER NOT NULL DEFAULT 0)" + ) + + // Add nullable folder_id column to playlists + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `folder_id` INTEGER" + ) + } + } + + val ALL_MIGRATIONS = arrayOf( + MIGRATION_1_2, + MIGRATION_2_3, + MIGRATION_3_4, + MIGRATION_4_5, + MIGRATION_5_6, + MIGRATION_6_7, + MIGRATION_7_8, + MIGRATION_8_9, + MIGRATION_9_10 + ) } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt index 9b62c1380..3787a58f5 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt @@ -29,6 +29,9 @@ open class PlaylistMetadataEntry( @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID) open val thumbnailStreamId: Long?, + @ColumnInfo(name = PlaylistEntity.PLAYLIST_FOLDER_ID) + open val folderId: Long?, + @ColumnInfo(name = PLAYLIST_STREAM_COUNT) open val streamCount: Long ) : PlaylistLocalItem { diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistFolderDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistFolderDAO.kt new file mode 100644 index 000000000..278a779e3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistFolderDAO.kt @@ -0,0 +1,27 @@ +package org.schabi.newpipe.database.playlist.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Update +import androidx.room.Insert +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Maybe +import org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity + +@Dao +interface PlaylistFolderDAO { + @Query("SELECT * FROM playlist_folders ORDER BY sort_order ASC") + fun getAll(): Flowable> + + @Insert + fun insert(folder: PlaylistFolderEntity): Long + + @Update + fun update(folder: PlaylistFolderEntity): Int + + @Query("DELETE FROM playlist_folders WHERE uid = :folderId") + fun delete(folderId: Long): Int + + @Query("SELECT * FROM playlist_folders WHERE uid = :folderId LIMIT 1") + fun get(folderId: Long): Maybe +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt index 8bf26d754..c88722337 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt @@ -71,9 +71,9 @@ interface PlaylistStreamDAO : BasicDAO { @Transaction @Query( """ - SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index, + SELECT uid, name, (SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id) AS thumbnail_url, - + display_index, is_thumbnail_permanent, thumbnail_stream_id, folder_id, COALESCE(COUNT(playlist_id), 0) AS streamCount FROM playlists LEFT JOIN playlist_stream_join @@ -106,8 +106,9 @@ interface PlaylistStreamDAO : BasicDAO { @Transaction @Query( """ - SELECT playlists.uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index, + SELECT playlists.uid, name, (SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id) AS thumbnail_url, + display_index, is_thumbnail_permanent, thumbnail_stream_id, folder_id, COALESCE(COUNT(playlist_id), 0) AS streamCount, COALESCE(SUM(url = :streamUrl), 0) AS timesStreamIsContained FROM playlists diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt index 4ea4eb3a7..36cedba0a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt @@ -26,9 +26,13 @@ data class PlaylistEntity @JvmOverloads constructor( @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID) var thumbnailStreamId: Long, - + + @ColumnInfo(name = PLAYLIST_FOLDER_ID) @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - var displayIndex: Long + var displayIndex: Long, + + @ColumnInfo(name = PLAYLIST_FOLDER_ID) + var folderId: Long? = null, ) { @Ignore @@ -46,6 +50,7 @@ data class PlaylistEntity @JvmOverloads constructor( const val PLAYLIST_TABLE = "playlists" const val PLAYLIST_ID = "uid" const val PLAYLIST_NAME = "name" + const val PLAYLIST_FOLDER_ID = "folder_id" const val PLAYLIST_THUMBNAIL_URL = "thumbnail_url" const val PLAYLIST_DISPLAY_INDEX = "display_index" const val PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent" diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistFolderEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistFolderEntity.kt new file mode 100644 index 000000000..5463d1c6b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistFolderEntity.kt @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = PlaylistFolderEntity.FOLDER_TABLE) +data class PlaylistFolderEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = FOLDER_ID) + var uid: Long = 0, + + @ColumnInfo(name = FOLDER_NAME) + var name: String, + + @ColumnInfo(name = FOLDER_SORT_ORDER) + var sortOrder: Long = 0 +){ + companion object { + const val FOLDER_TABLE = "playlist_folders" + const val FOLDER_ID = "uid" + const val FOLDER_NAME = "name" + const val FOLDER_SORT_ORDER = "sort_order" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index dcd19ebf9..27c35ff78 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -65,6 +65,8 @@ public final class BookmarkFragment extends BaseLocalListFragment { + java.util.List shown = new java.util.ArrayList<>(); + // virtual 'All' + shown.add(new org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity(-2, getString(R.string.tab_bookmarks), 0)); + // actual folders + if (folders != null) shown.addAll(folders); + // virtual 'Ungrouped' + shown.add(new org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity(-1, getString(R.string.no_playlist_bookmarked_yet), Long.MAX_VALUE)); + foldersAdapter.setFolders(shown); + }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, "Loading folders")))); + + final com.google.android.material.floatingactionbutton.FloatingActionButton fab = + rootView.findViewById(R.id.create_folder_fab); + fab.setOnClickListener(v -> { + final org.schabi.newpipe.databinding.DialogEditTextBinding dialogBinding = + org.schabi.newpipe.databinding.DialogEditTextBinding.inflate(getLayoutInflater()); + dialogBinding.dialogEditText.setHint(R.string.name); + new AlertDialog.Builder(activity) + .setView(dialogBinding.getRoot()) + .setPositiveButton(R.string.create_playlist, (d, w) -> { + final String name = dialogBinding.dialogEditText.getText().toString(); + disposables.add(playlistFolderManager.createFolder(name) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(id -> { }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, "Creating folder")))); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + }); } @Override @@ -190,10 +240,25 @@ public final class BookmarkFragment extends BaseLocalListFragment> localFlowable = + localPlaylistManager.getPlaylistsForFolder(selectedFolderId == null ? null : selectedFolderId) .onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getPlaylistsSubscriber()); + .observeOn(AndroidSchedulers.mainThread()); + + io.reactivex.rxjava3.core.Flowable> remoteFlowable = + remotePlaylistManager.getPlaylists() + .onBackpressureLatest() + .observeOn(AndroidSchedulers.mainThread()); + + io.reactivex.rxjava3.core.Flowable.combineLatest( + localFlowable, + remoteFlowable, + (local, remote) -> org.schabi.newpipe.local.bookmark.MergedPlaylistManager.merge((java.util.List)local, (java.util.List)remote) + ).subscribe(getPlaylistsSubscriber()); + } /////////////////////////////////////////////////////////////////////////// @@ -504,7 +569,9 @@ public final class BookmarkFragment extends BaseLocalListFragment items = new ArrayList<>(); items.add(rename); + final String assign = getString(R.string.add_to_playlist); items.add(delete); + items.add(assign); if (isThumbnailPermanent) { items.add(unsetThumbnail); } @@ -514,6 +581,48 @@ public final class BookmarkFragment extends BaseLocalListFragment folders = foldersAdapter == null ? new java.util.ArrayList<>() : foldersAdapter.getFolders(); + final java.util.List names = new java.util.ArrayList<>(); + names.add(getString(R.string.tab_bookmarks)); // All + for (org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity f : folders) { + // skip virtual entries if any + if (f.getUid() >= 0) names.add(f.getName()); + } + names.add(getString(R.string.no_playlist_bookmarked_yet)); // Ungrouped + + final String[] arr = names.toArray(new String[0]); + new AlertDialog.Builder(activity) + .setTitle(R.string.add_to_playlist) + .setItems(arr, (dialog, which) -> { + Long chosenFolderId = null; + if (which == 0) { + chosenFolderId = null; // All + } else if (which == arr.length - 1) { + chosenFolderId = -1L; // ungrouped (NULL) + } else { + // map to folders list (skip virtual All entry) + int idx = which - 1; + // find nth real folder + int count = 0; + for (org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity f : folders) { + if (f.getUid() >= 0) { + if (count == idx) { + chosenFolderId = f.getUid(); + break; + } + count++; + } + } + } + // set folder on playlist (null means no filtering/all) + Long dbFolderId = chosenFolderId != null && chosenFolderId.equals(-1L) ? null : chosenFolderId; + disposables.add(localPlaylistManager.setPlaylistFolder(selectedItem.getUid(), dbFolderId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, "Assigning folder")))); + }) + .show(); } else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) { final long thumbnailStreamId = localPlaylistManager .getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid()); @@ -559,4 +668,38 @@ public final class BookmarkFragment extends BaseLocalListFragment { + if (items[index].equals(rename)) { + // rename dialog + final org.schabi.newpipe.databinding.DialogEditTextBinding dialogBinding = + org.schabi.newpipe.databinding.DialogEditTextBinding.inflate(getLayoutInflater()); + dialogBinding.dialogEditText.setText(folderName); + new AlertDialog.Builder(activity) + .setView(dialogBinding.getRoot()) + .setPositiveButton(R.string.rename, (dd, w) -> { + final String newName = dialogBinding.dialogEditText.getText().toString(); + final org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity folder = new org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity(folderId, newName, 0); + disposables.add(playlistFolderManager.updateFolder(folder) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, "Renaming folder")))); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } else if (items[index].equals(delete)) { + disposables.add(playlistFolderManager.deleteFolder(folderId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK, "Deleting folder")))); + } + }) + .show(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/PlaylistFoldersAdapter.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/PlaylistFoldersAdapter.java new file mode 100644 index 000000000..12486b5dc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/PlaylistFoldersAdapter.java @@ -0,0 +1,86 @@ +package org.schabi.newpipe.local.bookmark; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity; + +import java.util.ArrayList; +import java.util.List; + +public class PlaylistFoldersAdapter extends RecyclerView.Adapter { + + public interface Listener { + void onFolderSelected(Long folderId); + void onFolderLongPressed(Long folderId, String name, int position); + } + + private final List folders = new ArrayList<>(); + private final Listener listener; + private int selectedPosition = -1; + + public PlaylistFoldersAdapter(final Listener listener) { + this.listener = listener; + } + + public void setFolders(final List list) { + folders.clear(); + if (list != null) folders.addAll(list); + notifyDataSetChanged(); + } + + public Long getSelectedFolderId() { + if (selectedPosition >= 0 && selectedPosition < folders.size()) { + return folders.get(selectedPosition).getUid(); + } + return null; + } + + public List getFolders() { + return folders; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_folder_item, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + final PlaylistFolderEntity folder = folders.get(position); + holder.name.setText(folder.getName()); + final Long id = folder.getUid(); + holder.itemView.setSelected(position == selectedPosition); + holder.itemView.setOnClickListener(v -> { + selectedPosition = position; + notifyDataSetChanged(); + listener.onFolderSelected(id); + }); + holder.itemView.setOnLongClickListener(v -> { + listener.onFolderLongPressed(id, folder.getName(), position); + return true; + }); + } + + @Override + public int getItemCount() { + return folders.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView name; + public ViewHolder(@NonNull View itemView) { + super(itemView); + name = itemView.findViewById(R.id.folder_name); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index 1480735fb..5de6dfeb3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -128,6 +128,23 @@ public class LocalPlaylistManager { return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); } + public Flowable> getPlaylistsForFolder(final Long folderId) { + return playlistStreamTable.getPlaylistMetadata() + .map(list -> { + if (folderId == null) return list; + java.util.List filtered = new java.util.ArrayList<>(); + for (PlaylistMetadataEntry e : list) { + if (e.folderId == null && folderId == -1L) { + // treat -1 as ungrouped + filtered.add(e); + } else if (e.folderId != null && e.folderId.equals(folderId)) { + filtered.add(e); + } + } + return filtered; + }).subscribeOn(Schedulers.io()); + } + public Flowable> getPlaylistStreams(final long playlistId) { return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); } @@ -136,6 +153,17 @@ public class LocalPlaylistManager { return modifyPlaylist(playlistId, name, THUMBNAIL_ID_LEAVE_UNCHANGED, false); } + public Maybe setPlaylistFolder(final long playlistId, final Long folderId) { + return playlistTable.getPlaylist(playlistId) + .firstElement() + .filter(playlistEntities -> !playlistEntities.isEmpty()) + .map(playlistEntities -> { + final PlaylistEntity playlist = playlistEntities.get(0); + playlist.setFolderId(folderId); + return playlistTable.update(playlist); + }).subscribeOn(Schedulers.io()); + } + public Maybe changePlaylistThumbnail(final long playlistId, final long thumbnailStreamId, final boolean isPermanent) { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/PlaylistFolderManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/PlaylistFolderManager.java new file mode 100644 index 000000000..3942ac90a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/PlaylistFolderManager.java @@ -0,0 +1,34 @@ +package org.schabi.newpipe.local.playlist; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity; + +public class PlaylistFolderManager { + private final AppDatabase database; + + public PlaylistFolderManager(final AppDatabase db) { + this.database = db; + } + + public Flowable> getFolders() { + return database.playlistFolderDAO().getAll(); + } + + public Maybe createFolder(final String name) { + return Maybe.fromCallable(() -> database.runInTransaction(() -> { + final PlaylistFolderEntity entity = new PlaylistFolderEntity(0, name, 0); + return database.playlistFolderDAO().insert(entity); + })); + } + + public Completable updateFolder(final PlaylistFolderEntity folder) { + return Completable.fromAction(() -> database.playlistFolderDAO().update(folder)); + } + + public Completable deleteFolder(final long folderId) { + return Completable.fromAction(() -> database.playlistFolderDAO().delete(folderId)); + } +} diff --git a/app/src/main/res/layout/fragment_bookmarks.xml b/app/src/main/res/layout/fragment_bookmarks.xml index 9767a1081..d623bf79e 100644 --- a/app/src/main/res/layout/fragment_bookmarks.xml +++ b/app/src/main/res/layout/fragment_bookmarks.xml @@ -5,10 +5,23 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + @@ -40,4 +53,14 @@ android:layout_alignParentTop="true" android:background="?attr/toolbar_shadow" /> + + diff --git a/app/src/main/res/layout/list_folder_item.xml b/app/src/main/res/layout/list_folder_item.xml new file mode 100644 index 000000000..a7543f0a6 --- /dev/null +++ b/app/src/main/res/layout/list_folder_item.xml @@ -0,0 +1,17 @@ + + From efb136e073509594095181445850e35c3a84398a Mon Sep 17 00:00:00 2001 From: James Cherished Date: Sun, 14 Dec 2025 21:10:34 +0000 Subject: [PATCH 2/2] test ran --- .../newpipe/database/DatabaseMigrationTest.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index 4327271f4..184750482 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -323,6 +323,42 @@ class DatabaseMigrationTest { assertEquals(-1, remoteListFromDB[1].displayIndex) } + @Test + fun migrateDatabaseFrom9to10() { + val databaseInV9 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_9) + + val localUid1: Long + databaseInV9.run { + localUid1 = insert( + "playlists", SQLiteDatabase.CONFLICT_FAIL, + ContentValues().apply { + put("name", DEFAULT_NAME + "1") + put("is_thumbnail_permanent", false) + put("thumbnail_stream_id", -1) + put("display_index", -1) + } + ) + close() + } + + testHelper.runMigrationsAndValidate( + AppDatabase.DATABASE_NAME, + Migrations.DB_VER_10, + true, + Migrations.MIGRATION_9_10 + ) + + val migratedDatabaseV10 = getMigratedDatabase() + val localListFromDB = migratedDatabaseV10.playlistDAO().getAll().blockingFirst() + + assertEquals(1, localListFromDB.size) + // folderId should exist and be null for existing playlists + assertNull(localListFromDB[0].folderId) + + val foldersFromDB = migratedDatabaseV10.playlistFolderDAO().getAll().blockingFirst() + assertEquals(0, foldersFromDB.size) + } + private fun getMigratedDatabase(): AppDatabase { val database: AppDatabase = Room.databaseBuilder( ApplicationProvider.getApplicationContext(),