Merge efb136e073509594095181445850e35c3a84398a into 61c25d458901e90bd02d35ec060f9217a4cdd251

This commit is contained in:
James Cherished 2026-01-21 06:26:57 +00:00 committed by GitHub
commit 81522375b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 479 additions and 12 deletions

View File

@ -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(),

View File

@ -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()
}

View File

@ -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

View File

@ -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
)
}

View File

@ -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 {

View File

@ -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>
}

View File

@ -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

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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) {

View File

@ -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));
}
}

View File

@ -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>

View 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" />