Merge efb136e073509594095181445850e35c3a84398a into 61c25d458901e90bd02d35ec060f9217a4cdd251
This commit is contained in:
commit
81522375b6
@ -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(),
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -349,4 +350,33 @@ object Migrations {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<List<PlaylistFolderEntity>>
|
||||
|
||||
@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<PlaylistFolderEntity>
|
||||
}
|
||||
@ -71,9 +71,9 @@ interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity> {
|
||||
@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<PlaylistStreamEntity> {
|
||||
@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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -65,6 +65,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private LocalPlaylistManager localPlaylistManager;
|
||||
private RemotePlaylistManager remotePlaylistManager;
|
||||
private org.schabi.newpipe.local.playlist.PlaylistFolderManager playlistFolderManager;
|
||||
private org.schabi.newpipe.local.bookmark.PlaylistFoldersAdapter foldersAdapter;
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
|
||||
/* Have the bookmarked playlists been fully loaded from db */
|
||||
@ -89,6 +91,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
final AppDatabase database = NewPipeDatabase.getInstance(activity);
|
||||
localPlaylistManager = new LocalPlaylistManager(database);
|
||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||
playlistFolderManager = new org.schabi.newpipe.local.playlist.PlaylistFolderManager(database);
|
||||
disposables = new CompositeDisposable();
|
||||
|
||||
isLoadingComplete = new AtomicBoolean();
|
||||
@ -128,6 +131,53 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
itemListAdapter.setUseItemHandle(true);
|
||||
final ComposeView emptyView = rootView.findViewById(R.id.empty_state_view);
|
||||
EmptyStateUtil.setEmptyStateComposable(emptyView, EmptyStateSpec.NoBookmarkedPlaylist);
|
||||
|
||||
final androidx.recyclerview.widget.RecyclerView folderBar = rootView.findViewById(R.id.folder_bar);
|
||||
foldersAdapter = new PlaylistFoldersAdapter(new PlaylistFoldersAdapter.Listener() {
|
||||
@Override
|
||||
public void onFolderSelected(Long folderId) {
|
||||
// reload playlists for selected folder
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFolderLongPressed(Long folderId, String name, int position) {
|
||||
// rename / delete dialog
|
||||
showFolderOptions(folderId, name);
|
||||
}
|
||||
});
|
||||
folderBar.setAdapter(foldersAdapter);
|
||||
// Observe folders and populate adapter (add virtual "All" and "Ungrouped")
|
||||
disposables.add(playlistFolderManager.getFolders()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(folders -> {
|
||||
java.util.List<org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity> 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<List<PlaylistL
|
||||
}
|
||||
isLoadingComplete.set(false);
|
||||
|
||||
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||
// Use current folder selection from adapter (if any), else null meaning All
|
||||
Long selectedFolderId = foldersAdapter == null ? null : foldersAdapter.getSelectedFolderId();
|
||||
|
||||
io.reactivex.rxjava3.core.Flowable<java.util.List<org.schabi.newpipe.database.playlist.PlaylistLocalItem>> localFlowable =
|
||||
localPlaylistManager.getPlaylistsForFolder(selectedFolderId == null ? null : selectedFolderId)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistsSubscriber());
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
|
||||
io.reactivex.rxjava3.core.Flowable<java.util.List<org.schabi.newpipe.database.playlist.PlaylistLocalItem>> 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<List<PlaylistL
|
||||
|
||||
final ArrayList<String> 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<List<PlaylistL
|
||||
showRenameDialog(selectedItem);
|
||||
} else if (items.get(index).equals(delete)) {
|
||||
showDeleteDialog(selectedItem.getOrderingName(), selectedItem);
|
||||
} else if (items.get(index).equals(assign)) {
|
||||
// Show folder choice dialog
|
||||
final java.util.List<org.schabi.newpipe.database.playlist.model.PlaylistFolderEntity> folders = foldersAdapter == null ? new java.util.ArrayList<>() : foldersAdapter.getFolders();
|
||||
final java.util.List<String> 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<List<PlaylistL
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showFolderOptions(final Long folderId, final String folderName) {
|
||||
final String rename = getString(R.string.rename);
|
||||
final String delete = getString(R.string.delete);
|
||||
|
||||
final String[] items = new String[] { rename, delete };
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(folderName)
|
||||
.setItems(items, (d, index) -> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PlaylistFoldersAdapter.ViewHolder> {
|
||||
|
||||
public interface Listener {
|
||||
void onFolderSelected(Long folderId);
|
||||
void onFolderLongPressed(Long folderId, String name, int position);
|
||||
}
|
||||
|
||||
private final List<PlaylistFolderEntity> folders = new ArrayList<>();
|
||||
private final Listener listener;
|
||||
private int selectedPosition = -1;
|
||||
|
||||
public PlaylistFoldersAdapter(final Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setFolders(final List<PlaylistFolderEntity> 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<PlaylistFolderEntity> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -128,6 +128,23 @@ public class LocalPlaylistManager {
|
||||
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<PlaylistMetadataEntry>> getPlaylistsForFolder(final Long folderId) {
|
||||
return playlistStreamTable.getPlaylistMetadata()
|
||||
.map(list -> {
|
||||
if (folderId == null) return list;
|
||||
java.util.List<PlaylistMetadataEntry> 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<List<PlaylistStreamEntry>> 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<Integer> 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<Integer> changePlaylistThumbnail(final long playlistId,
|
||||
final long thumbnailStreamId,
|
||||
final boolean isPermanent) {
|
||||
|
||||
@ -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<java.util.List<PlaylistFolderEntity>> getFolders() {
|
||||
return database.playlistFolderDAO().getAll();
|
||||
}
|
||||
|
||||
public Maybe<Long> 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));
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,23 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/folder_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:scrollbars="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layoutManager_orientation="horizontal"
|
||||
tools:listitem="@layout/list_folder_item" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/items_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/folder_bar"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="LinearLayoutManager"
|
||||
tools:listitem="@layout/list_playlist_mini_item" />
|
||||
@ -40,4 +53,14 @@
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="?attr/toolbar_shadow" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/create_folder_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="Create folder"
|
||||
app:srcCompat="@drawable/ic_playlist_add" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
17
app/src/main/res/layout/list_folder_item.xml
Normal file
17
app/src/main/res/layout/list_folder_item.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/folder_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginRight="6dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:backgroundTint="?attr/colorSurface"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1" />
|
||||
Loading…
x
Reference in New Issue
Block a user