mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2024-11-24 03:52:36 +01:00
Merge pull request #8221 from GGAutomaton/feature-7870
Sort bookmarked playlists
This commit is contained in:
commit
4c8238874e
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
@ -0,0 +1,730 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 9,
|
||||||
|
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,8 @@ import org.junit.Assert.assertNull
|
|||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
|
||||||
@ -22,13 +24,17 @@ class DatabaseMigrationTest {
|
|||||||
private const val DEFAULT_SERVICE_ID = 0
|
private const val DEFAULT_SERVICE_ID = 0
|
||||||
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||||
private const val DEFAULT_TITLE = "Test Title"
|
private const val DEFAULT_TITLE = "Test Title"
|
||||||
|
private const val DEFAULT_NAME = "Test Name"
|
||||||
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||||
private const val DEFAULT_DURATION = 480L
|
private const val DEFAULT_DURATION = 480L
|
||||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||||
|
|
||||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
||||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||||
|
|
||||||
|
private const val DEFAULT_THIRD_SERVICE_ID = 2
|
||||||
|
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
@ -115,6 +121,13 @@ class DatabaseMigrationTest {
|
|||||||
Migrations.MIGRATION_7_8
|
Migrations.MIGRATION_7_8
|
||||||
)
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_9,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
val migratedDatabaseV3 = getMigratedDatabase()
|
val migratedDatabaseV3 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||||
|
|
||||||
@ -198,6 +211,11 @@ class DatabaseMigrationTest {
|
|||||||
true, Migrations.MIGRATION_7_8
|
true, Migrations.MIGRATION_7_8
|
||||||
)
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
|
||||||
|
true, Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
val migratedDatabaseV8 = getMigratedDatabase()
|
val migratedDatabaseV8 = getMigratedDatabase()
|
||||||
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
|
||||||
|
|
||||||
@ -207,6 +225,94 @@ class DatabaseMigrationTest {
|
|||||||
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
|
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom8to9() {
|
||||||
|
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
|
||||||
|
|
||||||
|
val localUid1: Long
|
||||||
|
val localUid2: Long
|
||||||
|
val remoteUid1: Long
|
||||||
|
val remoteUid2: Long
|
||||||
|
databaseInV8.run {
|
||||||
|
localUid1 = insert(
|
||||||
|
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("name", DEFAULT_NAME + "1")
|
||||||
|
put("is_thumbnail_permanent", false)
|
||||||
|
put("thumbnail_stream_id", -1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
localUid2 = insert(
|
||||||
|
"playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("name", DEFAULT_NAME + "2")
|
||||||
|
put("is_thumbnail_permanent", false)
|
||||||
|
put("thumbnail_stream_id", -1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
delete(
|
||||||
|
"playlists", "uid = ?",
|
||||||
|
Array(1) { localUid1 }
|
||||||
|
)
|
||||||
|
remoteUid1 = insert(
|
||||||
|
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
remoteUid2 = insert(
|
||||||
|
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_SECOND_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
delete(
|
||||||
|
"remote_playlists", "uid = ?",
|
||||||
|
Array(1) { remoteUid2 }
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_9,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_8_9
|
||||||
|
)
|
||||||
|
|
||||||
|
val migratedDatabaseV9 = getMigratedDatabase()
|
||||||
|
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||||
|
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||||
|
|
||||||
|
assertEquals(1, localListFromDB.size)
|
||||||
|
assertEquals(localUid2, localListFromDB[0].uid)
|
||||||
|
assertEquals(-1, localListFromDB[0].displayIndex)
|
||||||
|
assertEquals(1, remoteListFromDB.size)
|
||||||
|
assertEquals(remoteUid1, remoteListFromDB[0].uid)
|
||||||
|
assertEquals(-1, remoteListFromDB[0].displayIndex)
|
||||||
|
|
||||||
|
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
|
||||||
|
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
|
||||||
|
)
|
||||||
|
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
||||||
|
PlaylistRemoteEntity(
|
||||||
|
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
|
||||||
|
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
|
||||||
|
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
|
||||||
|
assertEquals(2, localListFromDB.size)
|
||||||
|
assertEquals(localUid3, localListFromDB[1].uid)
|
||||||
|
assertEquals(-1, localListFromDB[1].displayIndex)
|
||||||
|
assertEquals(2, remoteListFromDB.size)
|
||||||
|
assertEquals(remoteUid3, remoteListFromDB[1].uid)
|
||||||
|
assertEquals(-1, remoteListFromDB[1].displayIndex)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getMigratedDatabase(): AppDatabase {
|
private fun getMigratedDatabase(): AppDatabase {
|
||||||
val database: AppDatabase = Room.databaseBuilder(
|
val database: AppDatabase = Room.databaseBuilder(
|
||||||
ApplicationProvider.getApplicationContext(),
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
@ -8,6 +8,7 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
|||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -28,7 +29,7 @@ public final class NewPipeDatabase {
|
|||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||||
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8)
|
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_8;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
||||||
|
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_8
|
version = DB_VER_9
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
@ -26,6 +26,7 @@ public final class Migrations {
|
|||||||
public static final int DB_VER_6 = 6;
|
public static final int DB_VER_6 = 6;
|
||||||
public static final int DB_VER_7 = 7;
|
public static final int DB_VER_7 = 7;
|
||||||
public static final int DB_VER_8 = 8;
|
public static final int DB_VER_8 = 8;
|
||||||
|
public static final int DB_VER_9 = 9;
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
@ -187,7 +188,7 @@ public final class Migrations {
|
|||||||
@Override
|
@Override
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||||
+ "INTEGER NOT NULL DEFAULT 0");
|
+ "INTEGER NOT NULL DEFAULT 0");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -245,6 +246,62 @@ public final class Migrations {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
try {
|
||||||
|
database.beginTransaction();
|
||||||
|
|
||||||
|
// Update playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
||||||
|
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL)");
|
||||||
|
database.execSQL("INSERT INTO `playlists_tmp` "
|
||||||
|
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "`display_index`) "
|
||||||
|
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
|
+ "-1 "
|
||||||
|
+ "FROM `playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
||||||
|
|
||||||
|
|
||||||
|
// Update remote_playlists.
|
||||||
|
// Create a temp table to initialize display_index.
|
||||||
|
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
|
||||||
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||||
|
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
||||||
|
+ "`display_index` INTEGER NOT NULL,"
|
||||||
|
+ "`stream_count` INTEGER)");
|
||||||
|
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
||||||
|
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
||||||
|
+ "`stream_count`)"
|
||||||
|
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
||||||
|
+ "-1, `stream_count` FROM `remote_playlists`");
|
||||||
|
|
||||||
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
|
// we don't need anymore.
|
||||||
|
database.execSQL("DROP TABLE `remote_playlists`");
|
||||||
|
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
||||||
|
|
||||||
|
// Create index on the new table.
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||||
|
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||||
|
|
||||||
|
database.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
database.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,17 @@ public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
|
|||||||
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||||
public final long timesStreamIsContained;
|
public final long timesStreamIsContained;
|
||||||
|
|
||||||
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||||
public PlaylistDuplicatesEntry(final long uid,
|
public PlaylistDuplicatesEntry(final long uid,
|
||||||
final String name,
|
final String name,
|
||||||
final String thumbnailUrl,
|
final String thumbnailUrl,
|
||||||
|
final boolean isThumbnailPermanent,
|
||||||
|
final long thumbnailStreamId,
|
||||||
|
final long displayIndex,
|
||||||
final long streamCount,
|
final long streamCount,
|
||||||
final long timesStreamIsContained) {
|
final long timesStreamIsContained) {
|
||||||
super(uid, name, thumbnailUrl, streamCount);
|
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
||||||
|
streamCount);
|
||||||
this.timesStreamIsContained = timesStreamIsContained;
|
this.timesStreamIsContained = timesStreamIsContained;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,13 @@
|
|||||||
package org.schabi.newpipe.database.playlist;
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
String getOrderingName();
|
String getOrderingName();
|
||||||
|
|
||||||
static List<PlaylistLocalItem> merge(
|
long getDisplayIndex();
|
||||||
final List<PlaylistMetadataEntry> localPlaylists,
|
|
||||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
long getUid();
|
||||||
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
|
|
||||||
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
void setDisplayIndex(long displayIndex);
|
||||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,40 @@ package org.schabi.newpipe.database.playlist;
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
|
|
||||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||||
|
|
||||||
@ColumnInfo(name = PLAYLIST_ID)
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
public final long uid;
|
private final long uid;
|
||||||
@ColumnInfo(name = PLAYLIST_NAME)
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
public final String name;
|
public final String name;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
|
private final boolean isThumbnailPermanent;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
private final long thumbnailStreamId;
|
||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
public final String thumbnailUrl;
|
public final String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
public final long streamCount;
|
public final long streamCount;
|
||||||
|
|
||||||
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||||
final long streamCount) {
|
final boolean isThumbnailPermanent, final long thumbnailStreamId,
|
||||||
|
final long displayIndex, final long streamCount) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,4 +48,27 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
|||||||
public String getOrderingName() {
|
public String getOrderingName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isThumbnailPermanent() {
|
||||||
|
return isThumbnailPermanent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getThumbnailStreamId() {
|
||||||
|
return thumbnailStreamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.dao;
|
|||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Transaction;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
@ -36,4 +37,17 @@ public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
|
|||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||||
Flowable<Long> getCount();
|
Flowable<Long> getCount();
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
default long upsertPlaylist(final PlaylistEntity playlist) {
|
||||||
|
final long playlistId = playlist.getUid();
|
||||||
|
|
||||||
|
if (playlistId == -1) {
|
||||||
|
// This situation is probably impossible.
|
||||||
|
return insert(playlist);
|
||||||
|
} else {
|
||||||
|
update(playlist);
|
||||||
|
return playlistId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||||
@ -31,10 +32,18 @@ public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
|||||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
|
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||||
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
|
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
|
||||||
|
|
||||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
|
@ -18,10 +18,12 @@ import io.reactivex.rxjava3.core.Flowable;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
||||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||||
@ -91,7 +93,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ","
|
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
@ -105,7 +109,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||||
+ " GROUP BY " + PLAYLIST_ID
|
+ " GROUP BY " + PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||||
|
|
||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
@ -126,8 +130,9 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
|
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||||
+ PLAYLIST_NAME + ", "
|
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||||
|
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||||
|
|
||||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||||
@ -149,6 +154,6 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||||||
+ " AND :streamUrl = :streamUrl"
|
+ " AND :streamUrl = :streamUrl"
|
||||||
|
|
||||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,15 @@ package org.schabi.newpipe.database.playlist.model;
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
import androidx.room.Index;
|
import androidx.room.Ignore;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
|
||||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
|
||||||
@Entity(tableName = PLAYLIST_TABLE,
|
@Entity(tableName = PLAYLIST_TABLE)
|
||||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
|
||||||
public class PlaylistEntity {
|
public class PlaylistEntity {
|
||||||
|
|
||||||
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||||
@ -22,6 +21,7 @@ public class PlaylistEntity {
|
|||||||
public static final String PLAYLIST_ID = "uid";
|
public static final String PLAYLIST_ID = "uid";
|
||||||
public static final String PLAYLIST_NAME = "name";
|
public static final String PLAYLIST_NAME = "name";
|
||||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||||
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
||||||
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
||||||
|
|
||||||
@ -38,11 +38,24 @@ public class PlaylistEntity {
|
|||||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
private long thumbnailStreamId;
|
private long thumbnailStreamId;
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex;
|
||||||
|
|
||||||
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
||||||
final long thumbnailStreamId) {
|
final long thumbnailStreamId, final long displayIndex) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||||
this.thumbnailStreamId = thumbnailStreamId;
|
this.thumbnailStreamId = thumbnailStreamId;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistEntity(final PlaylistMetadataEntry item) {
|
||||||
|
this.uid = item.getUid();
|
||||||
|
this.name = item.name;
|
||||||
|
this.isThumbnailPermanent = item.isThumbnailPermanent();
|
||||||
|
this.thumbnailStreamId = item.getThumbnailStreamId();
|
||||||
|
this.displayIndex = item.getDisplayIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
@ -77,4 +90,11 @@ public class PlaylistEntity {
|
|||||||
this.isThumbnailPermanent = isThumbnailSet;
|
this.isThumbnailPermanent = isThumbnailSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
|
|||||||
|
|
||||||
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
||||||
indices = {
|
indices = {
|
||||||
@Index(value = {REMOTE_PLAYLIST_NAME}),
|
|
||||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||||
})
|
})
|
||||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
@ -32,6 +31,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
public static final String REMOTE_PLAYLIST_URL = "url";
|
public static final String REMOTE_PLAYLIST_URL = "url";
|
||||||
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||||
|
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||||
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||||
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@ -53,6 +53,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||||
private String uploader;
|
private String uploader;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
private long displayIndex = -1; // Make sure the new item is on the top
|
||||||
|
|
||||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||||
private Long streamCount;
|
private Long streamCount;
|
||||||
|
|
||||||
@ -67,6 +70,19 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
this.streamCount = streamCount;
|
this.streamCount = streamCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
||||||
|
final String thumbnailUrl, final String uploader,
|
||||||
|
final long displayIndex, final Long streamCount) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.name = name;
|
||||||
|
this.url = url;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||||
@ -93,6 +109,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public long getUid() {
|
public long getUid() {
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
@ -141,6 +158,16 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
|||||||
this.uploader = uploader;
|
this.uploader = uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDisplayIndex() {
|
||||||
|
return displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDisplayIndex(final long displayIndex) {
|
||||||
|
this.displayIndex = displayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getStreamCount() {
|
public Long getStreamCount() {
|
||||||
return streamCount;
|
return streamCount;
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
public void commitPlaylistTabs() {
|
public void commitPlaylistTabs() {
|
||||||
pagerAdapter.getLocalPlaylistFragments()
|
pagerAdapter.getLocalPlaylistFragments()
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(LocalPlaylistFragment::commitChanges);
|
.forEach(LocalPlaylistFragment::saveImmediate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTabLayoutPosition() {
|
private void updateTabLayoutPosition() {
|
||||||
@ -282,7 +282,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||||||
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||||
* during runtime and changes are not committed immediately. However, in some cases,
|
* during runtime and changes are not committed immediately. However, in some cases,
|
||||||
* the changes need to be committed immediately by calling
|
* the changes need to be committed immediately by calling
|
||||||
* {@link LocalPlaylistFragment#commitChanges()}.
|
* {@link LocalPlaylistFragment#saveImmediate()}.
|
||||||
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||||
*/
|
*/
|
||||||
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||||
|
@ -482,7 +482,7 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
// commit previous pending changes to database
|
// commit previous pending changes to database
|
||||||
if (fragment instanceof LocalPlaylistFragment) {
|
if (fragment instanceof LocalPlaylistFragment) {
|
||||||
((LocalPlaylistFragment) fragment).commitChanges();
|
((LocalPlaylistFragment) fragment).saveImmediate();
|
||||||
} else if (fragment instanceof MainFragment) {
|
} else if (fragment instanceof MainFragment) {
|
||||||
((MainFragment) fragment).commitPlaylistTabs();
|
((MainFragment) fragment).commitPlaylistTabs();
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import org.schabi.newpipe.database.LocalItem;
|
|||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||||
@ -24,6 +25,7 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
|
|||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
||||||
|
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
||||||
@ -73,10 +75,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
||||||
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
|
||||||
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
|
||||||
|
private static final int LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x2003;
|
||||||
|
|
||||||
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
|
||||||
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
|
||||||
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
|
||||||
|
private static final int REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE = 0x3003;
|
||||||
|
|
||||||
private final LocalItemBuilder localItemBuilder;
|
private final LocalItemBuilder localItemBuilder;
|
||||||
private final ArrayList<LocalItem> localItems;
|
private final ArrayList<LocalItem> localItems;
|
||||||
@ -87,6 +91,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private View header = null;
|
private View header = null;
|
||||||
private View footer = null;
|
private View footer = null;
|
||||||
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
||||||
|
private boolean useItemHandle = false;
|
||||||
|
|
||||||
public LocalItemListAdapter(final Context context) {
|
public LocalItemListAdapter(final Context context) {
|
||||||
recordManager = new HistoryRecordManager(context);
|
recordManager = new HistoryRecordManager(context);
|
||||||
@ -180,6 +185,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
this.itemViewMode = itemViewMode;
|
this.itemViewMode = itemViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseItemHandle(final boolean useItemHandle) {
|
||||||
|
this.useItemHandle = useItemHandle;
|
||||||
|
}
|
||||||
|
|
||||||
public void setHeader(final View header) {
|
public void setHeader(final View header) {
|
||||||
final boolean changed = header != this.header;
|
final boolean changed = header != this.header;
|
||||||
this.header = header;
|
this.header = header;
|
||||||
@ -257,7 +266,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
final LocalItem item = localItems.get(position);
|
final LocalItem item = localItems.get(position);
|
||||||
switch (item.getLocalItemType()) {
|
switch (item.getLocalItemType()) {
|
||||||
case PLAYLIST_LOCAL_ITEM:
|
case PLAYLIST_LOCAL_ITEM:
|
||||||
if (itemViewMode == ItemViewMode.CARD) {
|
if (useItemHandle) {
|
||||||
|
return LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||||
|
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
@ -265,7 +276,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||||
}
|
}
|
||||||
case PLAYLIST_REMOTE_ITEM:
|
case PLAYLIST_REMOTE_ITEM:
|
||||||
if (itemViewMode == ItemViewMode.CARD) {
|
if (useItemHandle) {
|
||||||
|
return REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE;
|
||||||
|
} else if (itemViewMode == ItemViewMode.CARD) {
|
||||||
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
|
||||||
} else if (itemViewMode == ItemViewMode.GRID) {
|
} else if (itemViewMode == ItemViewMode.GRID) {
|
||||||
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
|
||||||
@ -314,12 +327,16 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
|
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
||||||
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
|
||||||
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
|
||||||
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
|
||||||
|
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
|
||||||
case STREAM_PLAYLIST_HOLDER_TYPE:
|
case STREAM_PLAYLIST_HOLDER_TYPE:
|
||||||
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
||||||
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package org.schabi.newpipe.local.bookmark;
|
package org.schabi.newpipe.local.bookmark;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
||||||
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -13,6 +16,8 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
@ -27,29 +32,45 @@ import org.schabi.newpipe.databinding.DialogEditTextBinding;
|
|||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
|
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
||||||
|
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
|
||||||
|
implements DebounceSavable {
|
||||||
|
|
||||||
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
@State
|
@State
|
||||||
protected Parcelable itemsListState;
|
Parcelable itemsListState;
|
||||||
|
|
||||||
private Subscription databaseSubscription;
|
private Subscription databaseSubscription;
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private LocalPlaylistManager localPlaylistManager;
|
private LocalPlaylistManager localPlaylistManager;
|
||||||
private RemotePlaylistManager remotePlaylistManager;
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
|
private ItemTouchHelper itemTouchHelper;
|
||||||
|
|
||||||
|
/* Have the bookmarked playlists been fully loaded from db */
|
||||||
|
private AtomicBoolean isLoadingComplete;
|
||||||
|
|
||||||
|
/* Gives enough time to avoid interrupting user sorting operations */
|
||||||
|
@Nullable
|
||||||
|
private DebounceSaver debounceSaver;
|
||||||
|
|
||||||
|
private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment LifeCycle - Creation
|
// Fragment LifeCycle - Creation
|
||||||
@ -65,6 +86,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
localPlaylistManager = new LocalPlaylistManager(database);
|
localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
isLoadingComplete = new AtomicBoolean();
|
||||||
|
debounceSaver = new DebounceSaver(3000, this);
|
||||||
|
|
||||||
|
deletedItems = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -91,10 +117,20 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
// Fragment LifeCycle - Views
|
// Fragment LifeCycle - Views
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
itemListAdapter.setUseItemHandle(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
|
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||||
|
|
||||||
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final LocalItem selectedItem) {
|
public void selected(final LocalItem selectedItem) {
|
||||||
@ -102,7 +138,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
|
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
|
||||||
entry.name);
|
entry.name);
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
@ -123,6 +159,14 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drag(final LocalItem selectedItem,
|
||||||
|
final RecyclerView.ViewHolder viewHolder) {
|
||||||
|
if (itemTouchHelper != null) {
|
||||||
|
itemTouchHelper.startDrag(viewHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,8 +178,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
public void startLoading(final boolean forceLoad) {
|
public void startLoading(final boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
if (debounceSaver != null) {
|
||||||
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
disposables.add(debounceSaver.getDebouncedSaver());
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
|
||||||
|
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(getPlaylistsSubscriber());
|
.subscribe(getPlaylistsSubscriber());
|
||||||
@ -149,6 +198,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
|
||||||
|
// Save on exit
|
||||||
|
saveImmediate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -163,19 +215,27 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
|
|
||||||
databaseSubscription = null;
|
databaseSubscription = null;
|
||||||
|
itemTouchHelper = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.getDebouncedSaveSignal().onComplete();
|
||||||
|
}
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debounceSaver = null;
|
||||||
disposables = null;
|
disposables = null;
|
||||||
localPlaylistManager = null;
|
localPlaylistManager = null;
|
||||||
remotePlaylistManager = null;
|
remotePlaylistManager = null;
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
|
|
||||||
|
isLoadingComplete = null;
|
||||||
|
deletedItems = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
@ -183,10 +243,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
||||||
return new Subscriber<List<PlaylistLocalItem>>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
showLoading();
|
showLoading();
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.cancel();
|
databaseSubscription.cancel();
|
||||||
}
|
}
|
||||||
@ -196,7 +258,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
public void onNext(final List<PlaylistLocalItem> subscriptions) {
|
||||||
handleResult(subscriptions);
|
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
||||||
|
handleResult(subscriptions);
|
||||||
|
isLoadingComplete.set(true);
|
||||||
|
}
|
||||||
if (databaseSubscription != null) {
|
if (databaseSubscription != null) {
|
||||||
databaseSubscription.request(1);
|
databaseSubscription.request(1);
|
||||||
}
|
}
|
||||||
@ -209,7 +274,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete() { }
|
public void onComplete() {
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,12 +310,183 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playlist Metadata Manipulation
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void changeLocalPlaylistName(final long id, final String name) {
|
||||||
|
if (localPlaylistManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
||||||
|
+ "with new name=[" + name + "] items");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
||||||
|
new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK,
|
||||||
|
"Changing playlist name")));
|
||||||
|
disposables.add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteItem(final PlaylistLocalItem item) {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
itemListAdapter.removeItem(item);
|
||||||
|
|
||||||
|
if (item instanceof PlaylistMetadataEntry) {
|
||||||
|
deletedItems.add(new Pair<>(item.getUid(),
|
||||||
|
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
|
||||||
|
} else if (item instanceof PlaylistRemoteEntity) {
|
||||||
|
deletedItems.add(new Pair<>(item.getUid(),
|
||||||
|
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.setHasChangesToSave();
|
||||||
|
saveImmediate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveImmediate() {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List must be loaded and modified in order to save
|
||||||
|
if (isLoadingComplete == null || debounceSaver == null
|
||||||
|
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LocalItem> items = itemListAdapter.getItemsList();
|
||||||
|
final List<PlaylistMetadataEntry> localItemsUpdate = new ArrayList<>();
|
||||||
|
final List<Long> localItemsDeleteUid = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remoteItemsUpdate = new ArrayList<>();
|
||||||
|
final List<Long> remoteItemsDeleteUid = new ArrayList<>();
|
||||||
|
|
||||||
|
// Calculate display index
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
final LocalItem item = items.get(i);
|
||||||
|
|
||||||
|
if (item instanceof PlaylistMetadataEntry
|
||||||
|
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
|
||||||
|
((PlaylistMetadataEntry) item).setDisplayIndex(i);
|
||||||
|
localItemsUpdate.add((PlaylistMetadataEntry) item);
|
||||||
|
} else if (item instanceof PlaylistRemoteEntity
|
||||||
|
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
|
||||||
|
((PlaylistRemoteEntity) item).setDisplayIndex(i);
|
||||||
|
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find deleted items
|
||||||
|
for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
|
||||||
|
if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
|
||||||
|
localItemsDeleteUid.add(item.first);
|
||||||
|
} else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
|
||||||
|
remoteItemsDeleteUid.add(item.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedItems.clear();
|
||||||
|
|
||||||
|
// 1. Update local playlists
|
||||||
|
// 2. Update remote playlists
|
||||||
|
// 3. Set NoChangesToSave
|
||||||
|
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
|
||||||
|
.mergeWith(remotePlaylistManager.updatePlaylists(
|
||||||
|
remoteItemsUpdate, remoteItemsDeleteUid))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
throwable -> showError(new ErrorInfo(throwable,
|
||||||
|
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
|
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
|
||||||
|
// with an `if (shouldUseGridLayout()) ...`
|
||||||
|
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||||
|
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||||
|
@Override
|
||||||
|
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
|
||||||
|
final int viewSize,
|
||||||
|
final int viewSizeOutOfBounds,
|
||||||
|
final int totalSize,
|
||||||
|
final long msSinceStartScroll) {
|
||||||
|
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
|
||||||
|
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||||
|
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||||
|
Math.abs(standardSpeed));
|
||||||
|
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(@NonNull final RecyclerView recyclerView,
|
||||||
|
@NonNull final RecyclerView.ViewHolder source,
|
||||||
|
@NonNull final RecyclerView.ViewHolder target) {
|
||||||
|
|
||||||
|
// Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder.
|
||||||
|
if (itemListAdapter == null
|
||||||
|
|| source.getItemViewType() != target.getItemViewType()
|
||||||
|
&& !(
|
||||||
|
(
|
||||||
|
(source instanceof LocalBookmarkPlaylistItemHolder)
|
||||||
|
|| (source instanceof RemoteBookmarkPlaylistItemHolder)
|
||||||
|
)
|
||||||
|
&& (
|
||||||
|
(target instanceof LocalBookmarkPlaylistItemHolder)
|
||||||
|
|| (target instanceof RemoteBookmarkPlaylistItemHolder)
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sourceIndex = source.getBindingAdapterPosition();
|
||||||
|
final int targetIndex = target.getBindingAdapterPosition();
|
||||||
|
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||||
|
if (isSwapped && debounceSaver != null) {
|
||||||
|
debounceSaver.setHasChangesToSave();
|
||||||
|
}
|
||||||
|
return isSwapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLongPressDragEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isItemViewSwipeEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||||
|
final int swipeDir) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// Utils
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||||
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
showDeleteDialog(item.getName(), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||||
@ -257,7 +494,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
final String delete = getString(R.string.delete);
|
final String delete = getString(R.string.delete);
|
||||||
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
|
||||||
final boolean isThumbnailPermanent = localPlaylistManager
|
final boolean isThumbnailPermanent = localPlaylistManager
|
||||||
.getIsPlaylistThumbnailPermanent(selectedItem.uid);
|
.getIsPlaylistThumbnailPermanent(selectedItem.getUid());
|
||||||
|
|
||||||
final ArrayList<String> items = new ArrayList<>();
|
final ArrayList<String> items = new ArrayList<>();
|
||||||
items.add(rename);
|
items.add(rename);
|
||||||
@ -270,13 +507,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
if (items.get(index).equals(rename)) {
|
if (items.get(index).equals(rename)) {
|
||||||
showRenameDialog(selectedItem);
|
showRenameDialog(selectedItem);
|
||||||
} else if (items.get(index).equals(delete)) {
|
} else if (items.get(index).equals(delete)) {
|
||||||
showDeleteDialog(selectedItem.name,
|
showDeleteDialog(selectedItem.name, selectedItem);
|
||||||
localPlaylistManager.deletePlaylist(selectedItem.uid));
|
|
||||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||||
final long thumbnailStreamId = localPlaylistManager
|
final long thumbnailStreamId = localPlaylistManager
|
||||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid);
|
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
|
||||||
localPlaylistManager
|
localPlaylistManager
|
||||||
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false)
|
.changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
@ -298,13 +534,13 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
.setView(dialogBinding.getRoot())
|
.setView(dialogBinding.getRoot())
|
||||||
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
|
||||||
changeLocalPlaylistName(
|
changeLocalPlaylistName(
|
||||||
selectedItem.uid,
|
selectedItem.getUid(),
|
||||||
dialogBinding.dialogEditText.getText().toString()))
|
dialogBinding.dialogEditText.getText().toString()))
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
|
||||||
if (activity == null || disposables == null) {
|
if (activity == null || disposables == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -313,35 +549,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||||||
.setTitle(name)
|
.setTitle(name)
|
||||||
.setMessage(R.string.delete_playlist_prompt)
|
.setMessage(R.string.delete_playlist_prompt)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setPositiveButton(R.string.delete, (dialog, i) ->
|
.setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
|
||||||
disposables.add(deleteReactor
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
|
|
||||||
showError(new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK,
|
|
||||||
"Deleting playlist")))))
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeLocalPlaylistName(final long id, final String name) {
|
|
||||||
if (localPlaylistManager == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "Updating playlist id=[" + id + "] "
|
|
||||||
+ "with new name=[" + name + "] items");
|
|
||||||
}
|
|
||||||
|
|
||||||
localPlaylistManager.renamePlaylist(id, name);
|
|
||||||
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
|
|
||||||
new ErrorInfo(throwable,
|
|
||||||
UserAction.REQUESTED_BOOKMARK,
|
|
||||||
"Changing playlist name")));
|
|
||||||
disposables.add(disposable);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
package org.schabi.newpipe.local.bookmark;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes care of remote and local playlists at once, hence "merged".
|
||||||
|
*/
|
||||||
|
public final class MergedPlaylistManager {
|
||||||
|
|
||||||
|
private MergedPlaylistManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Flowable<List<PlaylistLocalItem>> getMergedOrderedPlaylists(
|
||||||
|
final LocalPlaylistManager localPlaylistManager,
|
||||||
|
final RemotePlaylistManager remotePlaylistManager) {
|
||||||
|
return Flowable.combineLatest(
|
||||||
|
localPlaylistManager.getPlaylists(),
|
||||||
|
remotePlaylistManager.getPlaylists(),
|
||||||
|
MergedPlaylistManager::merge
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge localPlaylists and remotePlaylists by the display index.
|
||||||
|
* If two items have the same display index, sort them in {@code CASE_INSENSITIVE_ORDER}.
|
||||||
|
*
|
||||||
|
* @param localPlaylists local playlists, already sorted by display index
|
||||||
|
* @param remotePlaylists remote playlists, already sorted by display index
|
||||||
|
* @return merged playlists
|
||||||
|
*/
|
||||||
|
public static List<PlaylistLocalItem> merge(
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists,
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||||
|
|
||||||
|
// This algorithm is similar to the merge operation in merge sort.
|
||||||
|
final List<PlaylistLocalItem> result = new ArrayList<>(
|
||||||
|
localPlaylists.size() + remotePlaylists.size());
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex = new ArrayList<>();
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
int j = 0;
|
||||||
|
while (i < localPlaylists.size()) {
|
||||||
|
while (j < remotePlaylists.size()) {
|
||||||
|
if (remotePlaylists.get(j).getDisplayIndex()
|
||||||
|
<= localPlaylists.get(i).getDisplayIndex()) {
|
||||||
|
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
||||||
|
j++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addItem(result, localPlaylists.get(i), itemsWithSameIndex);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (j < remotePlaylists.size()) {
|
||||||
|
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
addItemsWithSameIndex(result, itemsWithSameIndex);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addItem(final List<PlaylistLocalItem> result,
|
||||||
|
final PlaylistLocalItem item,
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
||||||
|
if (!itemsWithSameIndex.isEmpty()
|
||||||
|
&& itemsWithSameIndex.get(0).getDisplayIndex() != item.getDisplayIndex()) {
|
||||||
|
// The new item has a different display index, add previous items with same
|
||||||
|
// index to the result.
|
||||||
|
addItemsWithSameIndex(result, itemsWithSameIndex);
|
||||||
|
itemsWithSameIndex.clear();
|
||||||
|
}
|
||||||
|
itemsWithSameIndex.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addItemsWithSameIndex(final List<PlaylistLocalItem> result,
|
||||||
|
final List<PlaylistLocalItem> itemsWithSameIndex) {
|
||||||
|
Collections.sort(itemsWithSameIndex,
|
||||||
|
Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||||
|
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
||||||
|
result.addAll(itemsWithSameIndex);
|
||||||
|
}
|
||||||
|
}
|
@ -155,14 +155,14 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||||||
|
|
||||||
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
|
playlistDisposables.add(manager.appendToPlaylist(playlist.getUid(), streams)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignored -> {
|
.subscribe(ignored -> {
|
||||||
successToast.show();
|
successToast.show();
|
||||||
|
|
||||||
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||||
playlistDisposables.add(manager
|
playlistDisposables.add(manager
|
||||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(),
|
.changePlaylistThumbnail(playlist.getUid(), streams.get(0).getUid(),
|
||||||
false)
|
false)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(ignore -> successToast.show()));
|
.subscribe(ignore -> successToast.show()));
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class LocalBookmarkPlaylistItemHolder extends LocalPlaylistItemHolder {
|
||||||
|
private final View itemHandleView;
|
||||||
|
|
||||||
|
public LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem,
|
||||||
|
final HistoryRecordManager historyRecordManager,
|
||||||
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
|
if (!(localItem instanceof PlaylistMetadataEntry)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||||
|
|
||||||
|
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) {
|
||||||
|
return (view, motionEvent) -> {
|
||||||
|
view.performClick();
|
||||||
|
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
|
||||||
|
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().drag(item,
|
||||||
|
LocalBookmarkPlaylistItemHolder.this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class RemoteBookmarkPlaylistItemHolder extends RemotePlaylistItemHolder {
|
||||||
|
private final View itemHandleView;
|
||||||
|
|
||||||
|
public RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteBookmarkPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
|
||||||
|
final ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem,
|
||||||
|
final HistoryRecordManager historyRecordManager,
|
||||||
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
|
if (!(localItem instanceof PlaylistRemoteEntity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||||
|
|
||||||
|
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) {
|
||||||
|
return (view, motionEvent) -> {
|
||||||
|
view.performClick();
|
||||||
|
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
|
||||||
|
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().drag(item,
|
||||||
|
RemoteBookmarkPlaylistItemHolder.this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import org.schabi.newpipe.util.ServiceHelper;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||||
|
|
||||||
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
|
||||||
final ViewGroup parent) {
|
final ViewGroup parent) {
|
||||||
super(infoItemBuilder, parent);
|
super(infoItemBuilder, parent);
|
||||||
|
@ -49,6 +49,8 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||||
|
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
@ -58,7 +60,6 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -68,12 +69,10 @@ import io.reactivex.rxjava3.core.Single;
|
|||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
|
||||||
|
|
||||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
||||||
implements PlaylistControlViewHolder {
|
implements PlaylistControlViewHolder, DebounceSavable {
|
||||||
/** Save the list 10 seconds after the last change occurred. */
|
|
||||||
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
|
||||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
@State
|
@State
|
||||||
protected Long playlistId;
|
protected Long playlistId;
|
||||||
@ -90,13 +89,12 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
private LocalPlaylistManager playlistManager;
|
private LocalPlaylistManager playlistManager;
|
||||||
private Subscription databaseSubscription;
|
private Subscription databaseSubscription;
|
||||||
|
|
||||||
private PublishSubject<Long> debouncedSaveSignal;
|
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
|
|
||||||
/** Whether the playlist has been fully loaded from db. */
|
/** Whether the playlist has been fully loaded from db. */
|
||||||
private AtomicBoolean isLoadingComplete;
|
private AtomicBoolean isLoadingComplete;
|
||||||
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
|
/** Used to debounce saving playlist edits to disk. */
|
||||||
private AtomicBoolean isModified;
|
private DebounceSaver debounceSaver;
|
||||||
/** Flag to prevent simultaneous rewrites of the playlist. */
|
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||||
private boolean isRewritingPlaylist = false;
|
private boolean isRewritingPlaylist = false;
|
||||||
|
|
||||||
@ -121,12 +119,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
public void onCreate(final Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||||
debouncedSaveSignal = PublishSubject.create();
|
|
||||||
|
|
||||||
disposables = new CompositeDisposable();
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
isLoadingComplete = new AtomicBoolean();
|
isLoadingComplete = new AtomicBoolean();
|
||||||
isModified = new AtomicBoolean();
|
debounceSaver = new DebounceSaver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -166,17 +163,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
return headerBinding;
|
return headerBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Commit changes immediately if the playlist has been modified.</p>
|
|
||||||
* Delete operations and other modifications will be committed to ensure that the database
|
|
||||||
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
|
|
||||||
*/
|
|
||||||
public void commitChanges() {
|
|
||||||
if (isModified != null && isModified.get()) {
|
|
||||||
saveImmediate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
@ -243,10 +229,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
}
|
}
|
||||||
disposables.add(getDebouncedSaver());
|
|
||||||
|
if (debounceSaver != null) {
|
||||||
|
disposables.add(debounceSaver.getDebouncedSaver());
|
||||||
|
debounceSaver.setNoChangesToSave();
|
||||||
|
}
|
||||||
|
|
||||||
isLoadingComplete.set(false);
|
isLoadingComplete.set(false);
|
||||||
isModified.set(false);
|
|
||||||
|
|
||||||
playlistManager.getPlaylistStreams(playlistId)
|
playlistManager.getPlaylistStreams(playlistId)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
@ -304,8 +293,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (debouncedSaveSignal != null) {
|
if (debounceSaver != null) {
|
||||||
debouncedSaveSignal.onComplete();
|
debounceSaver.getDebouncedSaveSignal().onComplete();
|
||||||
}
|
}
|
||||||
if (disposables != null) {
|
if (disposables != null) {
|
||||||
disposables.dispose();
|
disposables.dispose();
|
||||||
@ -314,12 +303,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
debouncedSaveSignal = null;
|
debounceSaver = null;
|
||||||
playlistManager = null;
|
playlistManager = null;
|
||||||
disposables = null;
|
disposables = null;
|
||||||
|
|
||||||
isLoadingComplete = null;
|
isLoadingComplete = null;
|
||||||
isModified = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
@ -343,7 +331,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
@Override
|
@Override
|
||||||
public void onNext(final List<PlaylistStreamEntry> streams) {
|
public void onNext(final List<PlaylistStreamEntry> streams) {
|
||||||
// Skip handling the result after it has been modified
|
// Skip handling the result after it has been modified
|
||||||
if (isModified == null || !isModified.get()) {
|
if (debounceSaver == null || !debounceSaver.getIsModified()) {
|
||||||
handleResult(streams);
|
handleResult(streams);
|
||||||
isLoadingComplete.set(true);
|
isLoadingComplete.set(true);
|
||||||
}
|
}
|
||||||
@ -495,7 +483,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
|
|
||||||
itemListAdapter.clearStreamItemList();
|
itemListAdapter.clearStreamItemList();
|
||||||
itemListAdapter.addItems(itemsToKeep);
|
itemListAdapter.addItems(itemsToKeep);
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
|
|
||||||
if (thumbnailVideoRemoved) {
|
if (thumbnailVideoRemoved) {
|
||||||
updateThumbnailUrl();
|
updateThumbnailUrl();
|
||||||
@ -666,7 +654,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
itemListAdapter.clearStreamItemList();
|
itemListAdapter.clearStreamItemList();
|
||||||
itemListAdapter.addItems(itemsToKeep);
|
itemListAdapter.addItems(itemsToKeep);
|
||||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
isRewritingPlaylist = false;
|
isRewritingPlaylist = false;
|
||||||
@ -685,41 +673,23 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveChanges() {
|
/**
|
||||||
if (isModified == null || debouncedSaveSignal == null) {
|
* <p>Commit changes immediately if the playlist has been modified.</p>
|
||||||
return;
|
* Delete operations and other modifications will be committed to ensure that the database
|
||||||
}
|
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
|
||||||
|
*/
|
||||||
isModified.set(true);
|
@Override
|
||||||
debouncedSaveSignal.onNext(System.currentTimeMillis());
|
public void saveImmediate() {
|
||||||
}
|
|
||||||
|
|
||||||
private Disposable getDebouncedSaver() {
|
|
||||||
if (debouncedSaveSignal == null) {
|
|
||||||
return Disposable.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return debouncedSaveSignal
|
|
||||||
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(ignored -> saveImmediate(), throwable ->
|
|
||||||
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
|
|
||||||
"Debounced saver")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveImmediate() {
|
|
||||||
if (playlistManager == null || itemListAdapter == null) {
|
if (playlistManager == null || itemListAdapter == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List must be loaded and modified in order to save
|
// List must be loaded and modified in order to save
|
||||||
if (isLoadingComplete == null || isModified == null
|
if (isLoadingComplete == null || debounceSaver == null
|
||||||
|| !isLoadingComplete.get() || !isModified.get()) {
|
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
|
||||||
Log.w(TAG, "Attempting to save playlist when local playlist "
|
|
||||||
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,8 +710,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() -> {
|
() -> {
|
||||||
if (isModified != null) {
|
if (debounceSaver != null) {
|
||||||
isModified.set(false);
|
debounceSaver.setNoChangesToSave();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
throwable -> showError(new ErrorInfo(throwable,
|
throwable -> showError(new ErrorInfo(throwable,
|
||||||
@ -784,7 +754,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
final int targetIndex = target.getBindingAdapterPosition();
|
final int targetIndex = target.getBindingAdapterPosition();
|
||||||
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||||
if (isSwapped) {
|
if (isSwapped) {
|
||||||
saveChanges();
|
debounceSaver.setHasChangesToSave();
|
||||||
}
|
}
|
||||||
return isSwapped;
|
return isSwapped;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import java.util.List;
|
|||||||
import io.reactivex.rxjava3.core.Completable;
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Maybe;
|
import io.reactivex.rxjava3.core.Maybe;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class LocalPlaylistManager {
|
public class LocalPlaylistManager {
|
||||||
@ -43,10 +42,13 @@ public class LocalPlaylistManager {
|
|||||||
return Maybe.empty();
|
return Maybe.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save to the database directly.
|
||||||
|
// Make sure the new playlist is always on the top of bookmark.
|
||||||
|
// The index will be reassigned to non-negative number in BookmarkFragment.
|
||||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||||
final List<Long> streamIds = streamTable.upsertAll(streams);
|
final List<Long> streamIds = streamTable.upsertAll(streams);
|
||||||
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
|
final PlaylistEntity newPlaylist = new PlaylistEntity(name, false,
|
||||||
streamIds.get(0));
|
streamIds.get(0), -1);
|
||||||
|
|
||||||
return insertJoinEntities(playlistTable.insert(newPlaylist),
|
return insertJoinEntities(playlistTable.insert(newPlaylist),
|
||||||
streamIds, 0);
|
streamIds, 0);
|
||||||
@ -89,8 +91,20 @@ public class LocalPlaylistManager {
|
|||||||
})).subscribeOn(Schedulers.io());
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
public Completable updatePlaylists(final List<PlaylistMetadataEntry> updateItems,
|
||||||
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
final List<Long> deletedItems) {
|
||||||
|
final List<PlaylistEntity> items = new ArrayList<>(updateItems.size());
|
||||||
|
for (final PlaylistMetadataEntry item : updateItems) {
|
||||||
|
items.add(new PlaylistEntity(item));
|
||||||
|
}
|
||||||
|
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
||||||
|
for (final Long uid : deletedItems) {
|
||||||
|
playlistTable.deletePlaylist(uid);
|
||||||
|
}
|
||||||
|
for (final PlaylistEntity item : items) {
|
||||||
|
playlistTable.upsertPlaylist(item);
|
||||||
|
}
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
|
public Flowable<List<PlaylistStreamEntry>> getDistinctPlaylistStreams(final long playlistId) {
|
||||||
@ -110,13 +124,12 @@ public class LocalPlaylistManager {
|
|||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
||||||
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<Integer> deletePlaylist(final long playlistId) {
|
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
||||||
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
|
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
||||||
.subscribeOn(Schedulers.io());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
||||||
|
@ -7,20 +7,23 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class RemotePlaylistManager {
|
public class RemotePlaylistManager {
|
||||||
|
|
||||||
|
private final AppDatabase database;
|
||||||
private final PlaylistRemoteDAO playlistRemoteTable;
|
private final PlaylistRemoteDAO playlistRemoteTable;
|
||||||
|
|
||||||
public RemotePlaylistManager(final AppDatabase db) {
|
public RemotePlaylistManager(final AppDatabase db) {
|
||||||
|
database = db;
|
||||||
playlistRemoteTable = db.playlistRemoteDAO();
|
playlistRemoteTable = db.playlistRemoteDAO();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
||||||
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io());
|
return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||||
@ -33,6 +36,18 @@ public class RemotePlaylistManager {
|
|||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Completable updatePlaylists(final List<PlaylistRemoteEntity> updateItems,
|
||||||
|
final List<Long> deletedItems) {
|
||||||
|
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
||||||
|
for (final Long uid: deletedItems) {
|
||||||
|
playlistRemoteTable.deletePlaylist(uid);
|
||||||
|
}
|
||||||
|
for (final PlaylistRemoteEntity item: updateItems) {
|
||||||
|
playlistRemoteTable.upsert(item);
|
||||||
|
}
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
||||||
return Single.fromCallable(() -> {
|
return Single.fromCallable(() -> {
|
||||||
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -31,7 +33,6 @@ import java.util.List;
|
|||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class SelectPlaylistFragment extends DialogFragment {
|
public class SelectPlaylistFragment extends DialogFragment {
|
||||||
@ -90,8 +91,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
|
final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
|
final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
|
|
||||||
disposable = Flowable.combineLatest(localPlaylistManager.getPlaylists(),
|
disposable = getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||||
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(this::displayPlaylists, this::onError);
|
.subscribe(this::displayPlaylists, this::onError);
|
||||||
}
|
}
|
||||||
@ -118,7 +118,7 @@ public class SelectPlaylistFragment extends DialogFragment {
|
|||||||
|
|
||||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
onSelectedListener.onLocalPlaylistSelected(entry.uid, entry.name);
|
onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name);
|
||||||
|
|
||||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package org.schabi.newpipe.util.debounce;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
|
|
||||||
|
public interface DebounceSavable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute operations to save the data. <br>
|
||||||
|
* Must set {@link DebounceSaver#setIsModified(boolean)} false in this method manually
|
||||||
|
* after the data has been saved.
|
||||||
|
*/
|
||||||
|
void saveImmediate();
|
||||||
|
|
||||||
|
void showError(ErrorInfo errorInfo);
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
package org.schabi.newpipe.util.debounce;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||||
|
|
||||||
|
public class DebounceSaver {
|
||||||
|
|
||||||
|
private final long saveDebounceMillis;
|
||||||
|
|
||||||
|
private final PublishSubject<Long> debouncedSaveSignal;
|
||||||
|
|
||||||
|
private final DebounceSavable debounceSavable;
|
||||||
|
|
||||||
|
// Has the object been modified
|
||||||
|
private final AtomicBoolean isModified;
|
||||||
|
|
||||||
|
// Default 10 seconds
|
||||||
|
private static final long DEFAULT_SAVE_DEBOUNCE_MILLIS = 10000;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code DebounceSaver}.
|
||||||
|
*
|
||||||
|
* @param saveDebounceMillis Save the object milliseconds later after the last change
|
||||||
|
* occurred.
|
||||||
|
* @param debounceSavable The object containing data to be saved.
|
||||||
|
*/
|
||||||
|
public DebounceSaver(final long saveDebounceMillis, final DebounceSavable debounceSavable) {
|
||||||
|
this.saveDebounceMillis = saveDebounceMillis;
|
||||||
|
debouncedSaveSignal = PublishSubject.create();
|
||||||
|
this.debounceSavable = debounceSavable;
|
||||||
|
this.isModified = new AtomicBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code DebounceSaver}. Save the object 10 seconds later after the last change
|
||||||
|
* occurred.
|
||||||
|
*
|
||||||
|
* @param debounceSavable The object containing data to be saved.
|
||||||
|
*/
|
||||||
|
public DebounceSaver(final DebounceSavable debounceSavable) {
|
||||||
|
this(DEFAULT_SAVE_DEBOUNCE_MILLIS, debounceSavable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getIsModified() {
|
||||||
|
return isModified.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNoChangesToSave() {
|
||||||
|
isModified.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublishSubject<Long> getDebouncedSaveSignal() {
|
||||||
|
return debouncedSaveSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Disposable getDebouncedSaver() {
|
||||||
|
return debouncedSaveSignal
|
||||||
|
.debounce(saveDebounceMillis, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> debounceSavable.saveImmediate(), throwable ->
|
||||||
|
debounceSavable.showError(new ErrorInfo(throwable,
|
||||||
|
UserAction.SOMETHING_ELSE, "Debounced saver")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasChangesToSave() {
|
||||||
|
if (isModified == null || debouncedSaveSignal == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isModified.set(true);
|
||||||
|
debouncedSaveSignal.onNext(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
}
|
84
app/src/main/res/layout/list_playlist_bookmark_item.xml
Normal file
84
app/src/main/res/layout/list_playlist_bookmark_item.xml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/itemRoot"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="@dimen/video_item_search_padding">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/itemThumbnailView"
|
||||||
|
android:layout_width="90dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/placeholder_thumbnail_playlist"
|
||||||
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/itemStreamCountView"
|
||||||
|
android:layout_width="45dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignTop="@id/itemThumbnailView"
|
||||||
|
android:layout_alignRight="@id/itemThumbnailView"
|
||||||
|
android:layout_alignBottom="@id/itemThumbnailView"
|
||||||
|
android:background="@color/playlist_stream_count_background_color"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="@color/duration_text_color"
|
||||||
|
android:textSize="@dimen/video_item_search_duration_text_size"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:drawableTint="@color/duration_text_color"
|
||||||
|
app:drawableTopCompat="@drawable/ic_playlist_play"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="3141" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/itemHandle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="55dp"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:contentDescription="@string/detail_drag_description"
|
||||||
|
android:paddingLeft="@dimen/video_item_search_image_right_margin"
|
||||||
|
android:scaleType="center"
|
||||||
|
app:srcCompat="@drawable/ic_drag_handle"
|
||||||
|
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/itemTitleView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_toStartOf="@id/itemHandle"
|
||||||
|
android:layout_toLeftOf="@id/itemHandle"
|
||||||
|
android:layout_toRightOf="@+id/itemThumbnailView"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:textSize="@dimen/video_item_search_title_text_size"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
|
||||||
|
|
||||||
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
android:id="@+id/itemUploaderView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/itemTitleView"
|
||||||
|
android:layout_toRightOf="@+id/itemThumbnailView"
|
||||||
|
android:lines="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textSize="@dimen/video_item_search_uploader_text_size"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="Uploader" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -0,0 +1,106 @@
|
|||||||
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.local.bookmark.MergedPlaylistManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PlaylistLocalItemTest {
|
||||||
|
@Test
|
||||||
|
public void emptyPlaylists() {
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
|
||||||
|
final List<PlaylistLocalItem> mergedPlaylists =
|
||||||
|
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
|
||||||
|
|
||||||
|
assertEquals(0, mergedPlaylists.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onlyLocalPlaylists() {
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
|
||||||
|
localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 0, 1));
|
||||||
|
localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 1, 1));
|
||||||
|
localPlaylists.add(new PlaylistMetadataEntry(3, "name3", "", false, -1, 3, 1));
|
||||||
|
final List<PlaylistLocalItem> mergedPlaylists =
|
||||||
|
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
|
||||||
|
|
||||||
|
assertEquals(3, mergedPlaylists.size());
|
||||||
|
assertEquals(0, mergedPlaylists.get(0).getDisplayIndex());
|
||||||
|
assertEquals(1, mergedPlaylists.get(1).getDisplayIndex());
|
||||||
|
assertEquals(3, mergedPlaylists.get(2).getDisplayIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onlyRemotePlaylists() {
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
|
||||||
|
remotePlaylists.add(new PlaylistRemoteEntity(
|
||||||
|
1, "name1", "url1", "", "", 1, 1L));
|
||||||
|
remotePlaylists.add(new PlaylistRemoteEntity(
|
||||||
|
2, "name2", "url2", "", "", 2, 1L));
|
||||||
|
remotePlaylists.add(new PlaylistRemoteEntity(
|
||||||
|
3, "name3", "url3", "", "", 4, 1L));
|
||||||
|
final List<PlaylistLocalItem> mergedPlaylists =
|
||||||
|
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
|
||||||
|
|
||||||
|
assertEquals(3, mergedPlaylists.size());
|
||||||
|
assertEquals(1, mergedPlaylists.get(0).getDisplayIndex());
|
||||||
|
assertEquals(2, mergedPlaylists.get(1).getDisplayIndex());
|
||||||
|
assertEquals(4, mergedPlaylists.get(2).getDisplayIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sameIndexWithDifferentName() {
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
|
||||||
|
localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 0, 1));
|
||||||
|
localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 1, 1));
|
||||||
|
remotePlaylists.add(new PlaylistRemoteEntity(
|
||||||
|
1, "name3", "url1", "", "", 0, 1L));
|
||||||
|
remotePlaylists.add(new PlaylistRemoteEntity(
|
||||||
|
2, "name4", "url2", "", "", 1, 1L));
|
||||||
|
final List<PlaylistLocalItem> mergedPlaylists =
|
||||||
|
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
|
||||||
|
|
||||||
|
assertEquals(4, mergedPlaylists.size());
|
||||||
|
assertTrue(mergedPlaylists.get(0) instanceof PlaylistMetadataEntry);
|
||||||
|
assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(0)).name);
|
||||||
|
assertTrue(mergedPlaylists.get(1) instanceof PlaylistRemoteEntity);
|
||||||
|
assertEquals("name3", ((PlaylistRemoteEntity) mergedPlaylists.get(1)).getName());
|
||||||
|
assertTrue(mergedPlaylists.get(2) instanceof PlaylistMetadataEntry);
|
||||||
|
assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(2)).name);
|
||||||
|
assertTrue(mergedPlaylists.get(3) instanceof PlaylistRemoteEntity);
|
||||||
|
assertEquals("name4", ((PlaylistRemoteEntity) mergedPlaylists.get(3)).getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sameNameWithDifferentIndex() {
|
||||||
|
final List<PlaylistMetadataEntry> localPlaylists = new ArrayList<>();
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists = new ArrayList<>();
|
||||||
|
localPlaylists.add(new PlaylistMetadataEntry(1, "name1", "", false, -1, 1, 1));
|
||||||
|
localPlaylists.add(new PlaylistMetadataEntry(2, "name2", "", false, -1, 3, 1));
|
||||||
|
remotePlaylists.add(new PlaylistRemoteEntity(
|
||||||
|
1, "name1", "url1", "", "", 0, 1L));
|
||||||
|
remotePlaylists.add(new PlaylistRemoteEntity(
|
||||||
|
2, "name2", "url2", "", "", 2, 1L));
|
||||||
|
final List<PlaylistLocalItem> mergedPlaylists =
|
||||||
|
MergedPlaylistManager.merge(localPlaylists, remotePlaylists);
|
||||||
|
|
||||||
|
assertEquals(4, mergedPlaylists.size());
|
||||||
|
assertTrue(mergedPlaylists.get(0) instanceof PlaylistRemoteEntity);
|
||||||
|
assertEquals("name1", ((PlaylistRemoteEntity) mergedPlaylists.get(0)).getName());
|
||||||
|
assertTrue(mergedPlaylists.get(1) instanceof PlaylistMetadataEntry);
|
||||||
|
assertEquals("name1", ((PlaylistMetadataEntry) mergedPlaylists.get(1)).name);
|
||||||
|
assertTrue(mergedPlaylists.get(2) instanceof PlaylistRemoteEntity);
|
||||||
|
assertEquals("name2", ((PlaylistRemoteEntity) mergedPlaylists.get(2)).getName());
|
||||||
|
assertTrue(mergedPlaylists.get(3) instanceof PlaylistMetadataEntry);
|
||||||
|
assertEquals("name2", ((PlaylistMetadataEntry) mergedPlaylists.get(3)).name);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user